@abi-software/flatmap-viewer 2.2.11-devel.2 → 2.2.12-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.9``
41
+ * ``npm install @abi-software/flatmap-viewer@2.2.12-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.11-devel.2",
3
+ "version": "2.2.12-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
@@ -123,7 +123,11 @@ export class PathControl
123
123
  if (checked != '') {
124
124
  this.__checkedCount += 1;
125
125
  }
126
- innerHTML.push(`<label for="path-${path.type}">${path.label}</label><div class="nerve-line nerve-${path.type}"></div><input id="path-${path.type}" type="checkbox" ${checked}/>`);
126
+ const colour = path.colour || '#440';
127
+ const style = path.dashed ? `background: repeating-linear-gradient(to right,${colour} 0,${colour} 6px,transparent 6px,transparent 9px);`
128
+ : `background: ${colour};`;
129
+
130
+ innerHTML.push(`<label for="path-${path.type}">${path.label}</label><div class="nerve-line" style="${style}"></div><input id="path-${path.type}" type="checkbox" ${checked}/>`);
127
131
  }
128
132
  this._legend.innerHTML = innerHTML.join('\n');
129
133
  this.__halfCount = Math.trunc(this.__pathTypes.length/2);
@@ -228,12 +232,9 @@ export class LayerControl
228
232
  {
229
233
  this.__map = map;
230
234
  this.__container = document.createElement('div');
231
- this.__container.className = 'maplibregl-ctrl';
232
- this.__container.id = 'flatmap-layer-control';
233
-
235
+ this.__container.className = 'maplibregl-ctrl flatmap-control';
234
236
  this.__layersControl = document.createElement('div');
235
- this.__layersControl.id = 'layer-control-text';
236
- this.__layersControl.className = 'flatmap-layer-grid';
237
+ this.__layersControl.className = 'flatmap-control-grid';
237
238
 
238
239
  const innerHTML = [];
239
240
  innerHTML.push(`<label for="layer-all-layers">ALL LAYERS:</label><input id="layer-all-layers" type="checkbox" checked/>`);
@@ -323,7 +324,6 @@ export class LayerControl
323
324
 
324
325
  //==============================================================================
325
326
 
326
-
327
327
  const SCKAN_STATES = [
328
328
  {
329
329
  'id': 'VALID',
@@ -338,11 +338,11 @@ const SCKAN_STATES = [
338
338
 
339
339
  export class SCKANControl
340
340
  {
341
- constructor(flatmap)
341
+ constructor(flatmap, options={sckan: 'valid'})
342
342
  {
343
343
  this.__flatmap = flatmap;
344
344
  this.__map = undefined;
345
- this.__state = 'valid';
345
+ this.__initialState = options.sckan || 'valid';
346
346
  }
347
347
 
348
348
  getDefaultPosition()
@@ -356,25 +356,22 @@ export class SCKANControl
356
356
  {
357
357
  this.__map = map;
358
358
  this.__container = document.createElement('div');
359
- this.__container.className = 'maplibregl-ctrl';
360
- this.__container.id = 'flatmap-layer-control';
361
-
359
+ this.__container.className = 'maplibregl-ctrl flatmap-control';
362
360
  this.__sckan = document.createElement('div');
363
- this.__sckan.id = 'sckan-control-text';
364
- this.__sckan.className = 'flatmap-layer-grid';
361
+ this.__sckan.className = 'flatmap-control-grid';
365
362
 
366
363
  const innerHTML = [];
367
- let checked = (this.__state === 'all') ? 'checked' : '';
364
+ let checked = (this.__initialState === 'all') ? 'checked' : '';
368
365
  innerHTML.push(`<label for="sckan-all-paths">ALL PATHS:</label><input id="sckan-all-paths" type="checkbox" ${checked}/>`);
369
366
  for (const state of SCKAN_STATES) {
370
- checked = (this.__state.toUpperCase() === state.id) ? 'checked' : '';
367
+ checked = (this.__initialState.toUpperCase() === state.id) ? 'checked' : '';
371
368
  innerHTML.push(`<label for="sckan-${state.id}">${state.description}</label><input id="sckan-${state.id}" type="checkbox" ${checked}/>`);
372
369
  }
373
370
  this.__sckan.innerHTML = innerHTML.join('\n');
374
371
 
375
372
  this.__sckanCount = SCKAN_STATES.length;
376
- this.__checkedCount = (this.__state === 'all') ? this.__sckanCount
377
- : (this.__state === 'none') ? 0
373
+ this.__checkedCount = (this.__initialState === 'all') ? this.__sckanCount
374
+ : (this.__initialState === 'none') ? 0
378
375
  : 1;
379
376
  this.__halfCount = Math.trunc(this.__sckanCount/2);
380
377
 
@@ -407,7 +404,8 @@ export class SCKANControl
407
404
  this.__container.appendChild(this.__sckan);
408
405
  this.__button.setAttribute('control-visible', 'true');
409
406
  const allLayersCheckbox = document.getElementById('sckan-all-paths');
410
- allLayersCheckbox.indeterminate = this.__state.toLowerCase().includes('valid');
407
+ allLayersCheckbox.indeterminate = (this.__checkedCount > 0)
408
+ && (this.__checkedCount < this.__sckanCount);
411
409
  this.__sckan.focus();
412
410
  } else {
413
411
  this.__sckan = this.__container.removeChild(this.__sckan);
@@ -430,11 +428,12 @@ export class SCKANControl
430
428
  const sckanCheckbox = document.getElementById(`sckan-${state.id}`);
431
429
  if (sckanCheckbox) {
432
430
  sckanCheckbox.checked = event.target.checked;
433
- }
431
+ this.__flatmap.enableSckanPath(state.id, event.target.checked);
432
+ }
434
433
  }
435
- this.__flatmap.showSckanPaths(this.__state);
436
434
  } else if (event.target.id.startsWith('sckan-')) {
437
435
  const sckanId = event.target.id.substring(6);
436
+ this.__flatmap.enableSckanPath(sckanId, event.target.checked);
438
437
  if (event.target.checked) {
439
438
  this.__checkedCount += 1;
440
439
  } else {
@@ -442,24 +441,14 @@ export class SCKANControl
442
441
  }
443
442
  const allLayersCheckbox = document.getElementById('sckan-all-paths');
444
443
  if (this.__checkedCount === 0) {
445
- this.__state = 'none';
446
444
  allLayersCheckbox.checked = false;
447
445
  allLayersCheckbox.indeterminate = false;
448
446
  } else if (this.__checkedCount === this.__sckanCount) {
449
- this.__state = 'all';
450
447
  allLayersCheckbox.checked = true;
451
448
  allLayersCheckbox.indeterminate = false;
452
449
  } else {
453
- if (event.target.checked) {
454
- this.__state = sckanId;
455
- } else if (sckanId === 'VALID') {
456
- this.__state = 'invalid';
457
- } else {
458
- this.__state = 'valid';
459
- }
460
450
  allLayersCheckbox.indeterminate = true;
461
451
  }
462
- this.__flatmap.showSckanPaths(this.__state);
463
452
  }
464
453
  }
465
454
  event.stopPropagation();
@@ -468,6 +457,61 @@ export class SCKANControl
468
457
 
469
458
  //==============================================================================
470
459
 
460
+ export class NerveControl
461
+ {
462
+ constructor(flatmap, options={showCentrelines: false})
463
+ {
464
+ this.__flatmap = flatmap;
465
+ this.__map = undefined;
466
+ this.__visible = options.showCentrelines || false;
467
+ }
468
+
469
+ getDefaultPosition()
470
+ //==================
471
+ {
472
+ return 'top-right';
473
+ }
474
+
475
+ onAdd(map)
476
+ //========
477
+ {
478
+ this.__map = map;
479
+ this.__container = document.createElement('div');
480
+ this.__container.className = 'maplibregl-ctrl';
481
+
482
+ this.__button = document.createElement('button');
483
+ this.__button.id = 'map-nerve-button';
484
+ this.__button.className = 'control-button text-button';
485
+ this.__button.setAttribute('type', 'button');
486
+ this.__button.setAttribute('aria-label', 'Show/hide nerve centrelines');
487
+ this.__button.textContent = 'NERVES';
488
+ this.__button.title = 'Show/hide nerve centrelines';
489
+ this.__container.appendChild(this.__button);
490
+
491
+ this.__container.addEventListener('click', this.onClick_.bind(this));
492
+ return this.__container;
493
+ }
494
+
495
+ onRemove()
496
+ //========
497
+ {
498
+ this.__container.parentNode.removeChild(this.__container);
499
+ this.__map = undefined;
500
+ }
501
+
502
+ onClick_(event)
503
+ //=============
504
+ {
505
+ if (event.target.id === 'map-nerve-button') {
506
+ this.__visible = !this.__visible;
507
+ this.__flatmap.enableCentrelines(this.__visible);
508
+ }
509
+ event.stopPropagation();
510
+ }
511
+ }
512
+
513
+ //==============================================================================
514
+
471
515
  export class BackgroundControl
472
516
  {
473
517
  constructor(flatmap)
@@ -275,21 +275,24 @@ class FlatMap
275
275
  }
276
276
 
277
277
  /**
278
- * @returns {Array.<{type: string, label: string, colour: string}>} an array of objects giving path types
279
- * with their descriptions and colours
278
+ * @returns {Array.<{type: string, label: string, colour: string}>} an array of objects giving the path types
279
+ * present in the map along with their
280
+ * descriptions and colours
280
281
  */
281
282
  pathTypes()
282
283
  //=========
283
284
  {
284
- return pathways.PATH_TYPES;
285
+ if (this._userInteractions !== null) {
286
+ return this._userInteractions.pathways.pathTypes();
287
+ }
285
288
  }
286
289
 
287
290
  /**
288
291
  * Hide or show paths of a given type.
289
292
  *
290
- * @param {string} pathType The path type
291
- * @param {boolean} [enable=true] If ``true`` then only show the paths
292
- * of the type otherwise only hide the paths
293
+ * @param {string} pathType The path type
294
+ * @param {boolean} enable Show or hide paths of that type. Defaults to
295
+ * ``true`` (show)
293
296
  */
294
297
  enablePath(pathType, enable=true)
295
298
  //===============================
@@ -300,32 +303,31 @@ class FlatMap
300
303
  }
301
304
 
302
305
  /**
303
- * Hide or show all paths except those of the given type.
306
+ * Hide or show all paths valid in SCKAN.
304
307
  *
305
- * @param {string|Array.<string>} pathTypes The path type(s)
306
- * @param {boolean} [enable=true] If ``true`` then only show the paths
307
- * of the type(s) otherwise only hide the paths
308
+ * @param {string} sckanState Either ``valid`` or ``invalid``
309
+ * @param {boolean} enable Show or hide paths with that SCKAN state.
310
+ * Defaults to ``true`` (show)
308
311
  */
309
- showPaths(pathTypes, enable=true)
310
- //===============================
312
+ enableSckanPath(sckanState, enable=true)
313
+ //======================================
311
314
  {
312
315
  if (this._userInteractions !== null) {
313
- this._userInteractions.showPaths(pathTypes, enable);
316
+ this._userInteractions.enableSckanPath(sckanState, enable);
314
317
  }
315
318
  }
316
319
 
317
320
  /**
318
- * Hide or show all paths valid in SCKAN.
321
+ * Hide or show centrelines and nodes.
319
322
  *
320
- * @param {string} validity Either ``VALID`` or ``INVALID``
321
- * @param {boolean} [enable=true] If ``true`` then only show the paths
322
- * of the type(s) otherwise only hide the paths
323
+ * @param {boolean} enable Show or centrelines and associated nodes.
324
+ * Defaults to ``true`` (show)
323
325
  */
324
- showSckanPaths(state='valid')
325
- //===========================
326
+ enableCentrelines(enable=true)
327
+ //============================
326
328
  {
327
329
  if (this._userInteractions !== null) {
328
- this._userInteractions.showSckanPaths(state);
330
+ this._userInteractions.enableCentrelines(enable);
329
331
  }
330
332
  }
331
333
 
@@ -664,18 +666,25 @@ class FlatMap
664
666
  }
665
667
  }
666
668
 
667
- setColour(options=null)
668
- //=====================
669
+ setPaint(options=null)
670
+ //====================
669
671
  {
670
672
  options = utils.setDefaults(options, {
671
673
  colour: true,
672
674
  outline: true
673
675
  });
674
676
  if (this._userInteractions !== null) {
675
- this._userInteractions.setColour(options);
677
+ this._userInteractions.setPaint(options);
676
678
  }
677
679
  }
678
680
 
681
+ setColour(options=null)
682
+ //=====================
683
+ {
684
+ console.log('`setColour()` is deprecated; please use `setPaint()` instead.')
685
+ this.setPaint(options);
686
+ }
687
+
679
688
  //==========================================================================
680
689
 
681
690
  /**
@@ -781,17 +790,17 @@ class FlatMap
781
790
  *
782
791
  * @param {string} anatomicalId The anatomical identifier of the feature on which
783
792
  * to place the marker
784
- * @param {string} [markerType=''] An optional parameter giving the type of marker
785
- * to use. Apart from the default, the only marker
786
- * type recognised is ``simulation``
793
+ * @param {string} [htmlElement=null] An optional parameter giving the DOM element to
794
+ * use as a marker. The default is a light blue,
795
+ * droplet-shaped SVG marker.
787
796
  * @return {integer} The identifier for the resulting marker. -1 is returned if the
788
797
  * map doesn't contain a feature with the given anatomical identifier
789
798
  */
790
- addMarker(anatomicalId, markerType='')
791
- //====================================
799
+ addMarker(anatomicalId, htmlElement=null)
800
+ //========================================
792
801
  {
793
802
  if (this._userInteractions !== null) {
794
- return this._userInteractions.addMarker(anatomicalId, markerType);
803
+ return this._userInteractions.addMarker(anatomicalId, htmlElement);
795
804
  }
796
805
  return -1;
797
806
  }
@@ -1272,9 +1281,9 @@ export class MapManager
1272
1281
  // Note the kind of map
1273
1282
 
1274
1283
  if ('style' in mapIndex) {
1275
- mapOptions.style = mapIndex.style; // Currently ``flatmap`` or ``fcdiagram``
1284
+ mapOptions.style = mapIndex.style; // Currently ``anatomical`` or ``functional``
1276
1285
  } else {
1277
- mapOptions.style = 'flatmap';
1286
+ mapOptions.style = 'flatmap'; // Default is a generic ``flatmap``
1278
1287
  }
1279
1288
 
1280
1289
  // Mapmaker has changed the name of the field to indicate that indicates if
package/src/info.js CHANGED
@@ -238,7 +238,11 @@ export class InfoControl
238
238
  if (prop in feature.properties) {
239
239
  const value = feature.properties[prop];
240
240
  if (value !== undefined) {
241
- values[prop] = value;
241
+ if (prop === 'label') {
242
+ values[prop] = value.replaceAll("\n", "<br/>");
243
+ } else {
244
+ values[prop] = value;
245
+ }
242
246
  }
243
247
  }
244
248
  });
@@ -37,8 +37,8 @@ import {ContextMenu} from './contextmenu.js';
37
37
  import {displayedProperties} from './info.js';
38
38
  import {InfoControl} from './info.js';
39
39
  import {LayerManager} from './layers.js';
40
- import {PATH_TYPES, PATHWAYS_LAYER, Pathways} from './pathways.js';
41
- import {BackgroundControl, LayerControl, PathControl, SCKANControl} from './controls.js';
40
+ import {PATHWAYS_LAYER, Pathways} from './pathways.js';
41
+ import {BackgroundControl, LayerControl, NerveControl, PathControl, SCKANControl} from './controls.js';
42
42
  import {SearchControl} from './search.js';
43
43
  import {VECTOR_TILES_SOURCE} from './styling.js';
44
44
 
@@ -118,13 +118,7 @@ export class UserInteractions
118
118
  this.__annotationByMarkerId = new Map();
119
119
 
120
120
  // Where to put labels and popups on a feature
121
- this.__centralPositions = new Map();
122
-
123
- // MapLibre dynamically sets a transform on marker elements so in
124
- // order to apply a scale transform we need to create marker icons
125
- // inside the marker container <div>.
126
- this._defaultMarkerHTML = new maplibre.Marker().getElement().innerHTML;
127
- this._simulationMarkerHTML = new maplibre.Marker({color: '#005974'}).getElement().innerHTML;
121
+ this.__markerPositions = new Map();
128
122
 
129
123
  // Fit the map to its initial position
130
124
 
@@ -139,7 +133,7 @@ export class UserInteractions
139
133
  this._pathways = new Pathways(flatmap);
140
134
 
141
135
  // The path types in this map
142
- const mapPathTypes = this._pathways.pathTypes;
136
+ const mapPathTypes = this._pathways.pathTypes();
143
137
 
144
138
  // Disable paths that are not initially shown
145
139
  for (const path of mapPathTypes) {
@@ -167,9 +161,15 @@ export class UserInteractions
167
161
  // Add a control to manage our layers
168
162
  this._map.addControl(new LayerControl(flatmap, this._layerManager));
169
163
 
164
+ // Add a control for nerve centrelines if they are present
165
+ if (this._pathways.haveCentrelines) {
166
+ this._map.addControl(new NerveControl(flatmap, this._layerManager, {showCentrelines: false}));
167
+ this.enableCentrelines(false);
168
+ }
169
+
170
170
  // A SCKAN path control for FC maps
171
- if (flatmap.options.style === 'fcdiagram') {
172
- this._map.addControl(new SCKANControl(flatmap));
171
+ if (flatmap.options.style === 'functional') {
172
+ this._map.addControl(new SCKANControl(flatmap, flatmap.options.layerOptions));
173
173
  }
174
174
  }
175
175
 
@@ -213,6 +213,12 @@ export class UserInteractions
213
213
  this.__pan_zoom_enabled = false;
214
214
  }
215
215
 
216
+ get pathways()
217
+ //============
218
+ {
219
+ return this._pathways;
220
+ }
221
+
216
222
  getState()
217
223
  //========
218
224
  {
@@ -246,11 +252,11 @@ export class UserInteractions
246
252
  }
247
253
  }
248
254
 
249
- setColour(options)
250
- //================
255
+ setPaint(options)
256
+ //===============
251
257
  {
252
258
  this.__colourOptions = options;
253
- this._layerManager.setColour(options);
259
+ this._layerManager.setPaint(options);
254
260
  }
255
261
 
256
262
  getLayers()
@@ -291,7 +297,7 @@ export class UserInteractions
291
297
  {
292
298
  featureId = +featureId; // Ensure numeric
293
299
  if (this._selectedFeatureIds.size === 0) {
294
- this._layerManager.setColour({...this.__colourOptions, dimmed: dim});
300
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: dim});
295
301
  }
296
302
  if (this._selectedFeatureIds.has(featureId)) {
297
303
  this._selectedFeatureIds.set(featureId, this._selectedFeatureIds.get(featureId) + 1);
@@ -321,7 +327,7 @@ export class UserInteractions
321
327
  }
322
328
  }
323
329
  if (this._selectedFeatureIds.size === 0) {
324
- this._layerManager.setColour({...this.__colourOptions, dimmed: false});
330
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: false});
325
331
  }
326
332
  }
327
333
 
@@ -335,7 +341,7 @@ export class UserInteractions
335
341
  }
336
342
  }
337
343
  this._selectedFeatureIds.clear();
338
- this._layerManager.setColour({...this.__colourOptions, dimmed: false});
344
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: false});
339
345
  }
340
346
 
341
347
  activeFeaturesAtEvent_(event)
@@ -609,7 +615,7 @@ export class UserInteractions
609
615
  location = this.__lastClickLngLat;
610
616
  } else {
611
617
  // Position popup at the feature's 'centre'
612
- location = this.__centralPosition(featureId, ann);
618
+ location = this.__markerPosition(featureId, ann);
613
619
  }
614
620
 
615
621
  // Make sure the feature is on screen
@@ -651,9 +657,13 @@ export class UserInteractions
651
657
  const tooltips = [];
652
658
  for (const lineFeature of lineFeatures) {
653
659
  const properties = lineFeature.properties;
654
- if ('label' in properties
655
- && (!('tooltip' in properties) || properties.tooltip)
656
- && !('labelled' in properties)) {
660
+ if ('error' in properties) {
661
+ tooltips.push(`<div class="feature-error">Error: ${properties.error}</div>`)
662
+ }
663
+ if ('warning' in properties) {
664
+ tooltips.push(`<div class="feature-error">Warning: ${properties.warning}</div>`)
665
+ }
666
+ if ('label' in properties && (!('tooltip' in properties) || properties.tooltip)) {
657
667
  let tooltip = '';
658
668
  const label = properties.label;
659
669
  const cleanLabel = (label.substr(0, 1).toUpperCase() + label.substr(1)).replaceAll("\n", "<br/>");
@@ -662,32 +672,37 @@ export class UserInteractions
662
672
  }
663
673
  }
664
674
  }
665
- if (tooltips.length === 0) {
666
- return '';
667
- }
668
- return `<div class='flatmap-feature-label'>${tooltips.join('<hr/>')}</div>`;
675
+ return (tooltips.length === 0) ? ''
676
+ : `<div class='flatmap-feature-label'>${tooltips.join('<hr/>')}</div>`;
669
677
  }
670
678
 
671
679
  tooltipHtml_(properties, forceLabel=false)
672
680
  //========================================
673
681
  {
682
+ const tooltip = [];
683
+ if ('error' in properties) {
684
+ tooltip.push(`<div class="feature-error">Error: ${properties.error}</div>`)
685
+ }
686
+ if ('warning' in properties) {
687
+ tooltip.push(`<div class="feature-error">Warning: ${properties.warning}</div>`)
688
+ }
674
689
  if (('label' in properties || 'hyperlink' in properties)
675
- && (forceLabel || !('tooltip' in properties) || properties.tooltip)
676
- && !('labelled' in properties)) {
690
+ && (forceLabel || !('tooltip' in properties) || properties.tooltip)) {
677
691
  const label = ('label' in properties) ? (properties.label.substr(0, 1).toUpperCase()
678
692
  + properties.label.substr(1)).replaceAll("\n", "<br/>")
679
693
  : '';
680
694
  if ('hyperlink' in properties) {
681
695
  if (label === '') {
682
- return `<div class='flatmap-feature-label'><a href='${properties.hyperlink}'>${properties.hyperlink}</a></div>`;
696
+ tooltip.push(`<a href='${properties.hyperlink}'>${properties.hyperlink}</a>`);
683
697
  } else {
684
- return `<div class='flatmap-feature-label'><a href='${properties.hyperlink}'>${label}</a></div>`;
698
+ tooltip.push(`<a href='${properties.hyperlink}'>${label}</a></div>`);
685
699
  }
686
700
  } else {
687
- return `<div class='flatmap-feature-label'>${label}</div>`;
701
+ tooltip.push(label);
688
702
  }
689
703
  }
690
- return '';
704
+ return (tooltip.length === 0) ? ''
705
+ : `<div class='flatmap-feature-label'>${tooltip.join('<hr/>')}</div>`;
691
706
  }
692
707
 
693
708
  __featureEvent(type, feature)
@@ -976,6 +991,7 @@ export class UserInteractions
976
991
  togglePaths()
977
992
  //===========
978
993
  {
994
+ console.log('Depracated API function called: togglePaths()')
979
995
  if (this._disabledPathFeatures){
980
996
  this.enablePathFeatures_(true, this._pathways.allFeatureIds());
981
997
  this._disabledPathFeatures = false;
@@ -990,29 +1006,6 @@ export class UserInteractions
990
1006
  this.enablePathFeatures_(enable, this._pathways.typeFeatureIds(pathType));
991
1007
  }
992
1008
 
993
- showPaths(pathTypes, enable=true)
994
- //===============================
995
- {
996
- // Disable/enable all paths except those with `pathTypes`
997
- if (Array.isArray(pathTypes)) {
998
- for (const pathType of pathways.PATH_TYPES) {
999
- if (pathTypes.indexOf(pathType.type) >= 0) {
1000
- this.enablePath(pathType.type, enable)
1001
- } else {
1002
- this.enablePath(pathType.type, !enable)
1003
- }
1004
- }
1005
- } else {
1006
- for (const pathType of pathways.PATH_TYPES) {
1007
- if (pathType.type === pathTypes) {
1008
- this.enablePath(pathType.type, enable)
1009
- } else {
1010
- this.enablePath(pathType.type, !enable)
1011
- }
1012
- }
1013
- }
1014
- }
1015
-
1016
1009
  pathwaysFeatureIds(externalIds)
1017
1010
  //=============================
1018
1011
  {
@@ -1028,52 +1021,57 @@ export class UserInteractions
1028
1021
  return this._pathways.nodePathModels(nodeId);
1029
1022
  }
1030
1023
 
1031
- showSckanPaths(state='valid')
1032
- //===========================
1024
+ enableCentrelines(show=true)
1025
+ //==========================
1033
1026
  {
1034
- this._layerManager.setFilter({sckan: state});
1027
+ this.enablePath('centreline', show);
1028
+ this._layerManager.setPaint({showCentrelines: show});
1029
+ }
1030
+
1031
+ enableSckanPath(sckanState, enable=true)
1032
+ //======================================
1033
+ {
1034
+ this._layerManager.enableSckanPath(sckanState, enable);
1035
1035
  }
1036
1036
 
1037
1037
  //==============================================================================
1038
1038
 
1039
- // Find where to place a label or popup on a feature
1039
+ // Marker handling
1040
1040
 
1041
- __centralPosition(featureId, annotation)
1042
- //======================================
1041
+ __markerPosition(featureId, annotation)
1043
1042
  {
1044
- if (this.__centralPositions.has(featureId)) {
1045
- return this.__centralPositions.get(featureId);
1043
+ if (this.__markerPositions.has(featureId)) {
1044
+ return this.__markerPositions.get(featureId);
1046
1045
  }
1047
- let position = annotation.centroid;
1048
- const features = this._map.querySourceFeatures(VECTOR_TILES_SOURCE, {
1049
- 'sourceLayer': this._flatmap.options.separateLayers
1050
- ? `${annotation['layer']}_${annotation['tile-layer']}`
1051
- : annotation['tile-layer'],
1052
- 'filter': [
1053
- 'all',
1054
- [ '==', ['id'], parseInt(featureId) ],
1055
- [ '==', ['geometry-type'], 'Polygon' ]
1056
- ]
1057
- });
1058
- if (features.length > 0) {
1059
- const feature = features[0];
1060
- const polygon = feature.geometry.coordinates;
1061
- // Rough heuristic. Area is in km^2; below appears to be good enough.
1062
- const precision = ('area' in feature.properties)
1063
- ? Math.sqrt(feature.properties.area)/500000
1064
- : 0.1;
1065
- position = polylabel(polygon, precision);
1046
+ let position = annotation.markerPosition || annotation.centroid;
1047
+ if (position === null || position == undefined) {
1048
+ // Find where to place a label or popup on a feature
1049
+ const features = this._map.querySourceFeatures(VECTOR_TILES_SOURCE, {
1050
+ 'sourceLayer': this._flatmap.options.separateLayers
1051
+ ? `${annotation['layer']}_${annotation['tile-layer']}`
1052
+ : annotation['tile-layer'],
1053
+ 'filter': [
1054
+ 'all',
1055
+ [ '==', ['id'], parseInt(featureId) ],
1056
+ [ '==', ['geometry-type'], 'Polygon' ]
1057
+ ]
1058
+ });
1059
+ if (features.length > 0) {
1060
+ const feature = features[0];
1061
+ const polygon = feature.geometry.coordinates;
1062
+ // Rough heuristic. Area is in km^2; below appears to be good enough.
1063
+ const precision = ('area' in feature.properties)
1064
+ ? Math.sqrt(feature.properties.area)/500000
1065
+ : 0.1;
1066
+ position = polylabel(polygon, precision);
1067
+ }
1066
1068
  }
1067
- this.__centralPositions.set(featureId, position);
1069
+ this.__markerPositions.set(featureId, position);
1068
1070
  return position;
1069
1071
  }
1070
1072
 
1071
- //==============================================================================
1072
-
1073
- // Marker handling
1074
-
1075
- addMarker(anatomicalId, markerType='')
1076
- //====================================
1073
+ addMarker(anatomicalId, htmlElement=null)
1074
+ //=======================================
1077
1075
  {
1078
1076
  const featureIds = this._flatmap.modelFeatureIds(anatomicalId);
1079
1077
  let markerId = -1;
@@ -1089,19 +1087,19 @@ export class UserInteractions
1089
1087
  markerId = this.__lastMarkerId;
1090
1088
  }
1091
1089
 
1090
+ // MapLibre dynamically sets a transform on marker elements so in
1091
+ // order to apply a scale transform we need to create marker icons
1092
+ // inside the marker container <div>.
1093
+ const markerHTML = htmlElement ? new maplibre.Marker({element: htmlElement})
1094
+ : new maplibre.Marker();
1095
+
1092
1096
  const markerElement = document.createElement('div');
1093
1097
  const markerIcon = document.createElement('div');
1094
- if (markerType === 'simulation') {
1095
- markerIcon.innerHTML = this._simulationMarkerHTML;
1096
- } else {
1097
- markerIcon.innerHTML = this._defaultMarkerHTML;
1098
- }
1098
+ markerIcon.innerHTML = markerHTML.getElement().innerHTML;
1099
1099
  markerIcon.className = 'flatmap-marker';
1100
1100
  markerElement.appendChild(markerIcon);
1101
1101
 
1102
- const markerPosition = (annotation.geometry === 'Polygon')
1103
- ? this.__centralPosition(featureId, annotation)
1104
- : annotation.centroid;
1102
+ const markerPosition = this.__markerPosition(featureId, annotation);
1105
1103
  const marker = new maplibre.Marker(markerElement)
1106
1104
  .setLngLat(markerPosition)
1107
1105
  .addTo(this._map);
package/src/layers.js CHANGED
@@ -93,14 +93,12 @@ class MapStylingLayers
93
93
  : sourceLayer;
94
94
  }
95
95
 
96
- setColour(options)
96
+ setPaint(options)
97
97
  {
98
-
99
98
  }
100
99
 
101
100
  setFilter(options)
102
101
  {
103
-
104
102
  }
105
103
  }
106
104
 
@@ -124,6 +122,8 @@ class MapFeatureLayers extends MapStylingLayers
124
122
  this.__addStyleLayer(style.FeatureDashLineLayer);
125
123
  this.__addStyleLayer(style.FeatureLineLayer);
126
124
  this.__addStyleLayer(style.FeatureBorderLayer);
125
+ this.__addStyleLayer(style.CentrelineNodeFillLayer);
126
+ this.__addStyleLayer(style.CentrelineNodeBorderLayer);
127
127
  }
128
128
  this.__addPathwayStyleLayers();
129
129
  if (vectorFeatures) {
@@ -134,9 +134,9 @@ class MapFeatureLayers extends MapStylingLayers
134
134
  }
135
135
  }
136
136
 
137
- // Make sure our colour options are set properly, in particular raster layer visibility
137
+ // Make sure our paint options are set properly, in particular raster layer visibility
138
138
 
139
- this.setColour(this.__layerOptions);
139
+ this.setPaint(this.__layerOptions);
140
140
  }
141
141
 
142
142
  __addStyleLayer(styleClass, sourceLayer=FEATURES_LAYER)
@@ -167,8 +167,8 @@ class MapFeatureLayers extends MapStylingLayers
167
167
  }
168
168
  }
169
169
 
170
- setColour(options)
171
- //================
170
+ setPaint(options)
171
+ //===============
172
172
  {
173
173
  for (const layer of this.__layers) {
174
174
  const paintStyle = layer.paintStyle(options, true);
@@ -208,8 +208,8 @@ class MapRasterLayers extends MapStylingLayers
208
208
  this.__map.addLayer(styleLayer.style(this.__layerOptions));
209
209
  this.__layers.push(styleLayer);
210
210
  }
211
- // Make sure our colour options are set properly, in particular raster layer visibility
212
- this.setColour(this.__layerOptions);
211
+ // Make sure our paint options are set properly, in particular raster layer visibility
212
+ this.setPaint(this.__layerOptions);
213
213
  }
214
214
 
215
215
  addLayer(layer)
@@ -220,12 +220,12 @@ class MapRasterLayers extends MapStylingLayers
220
220
  this.__map.addLayer(rasterLayer.style(this.__layerOptions));
221
221
  this.__layers.push(rasterLayer);
222
222
  }
223
- // Make sure our colour options are set properly, in particular raster layer visibility
224
- this.setColour(this.__layerOptions);
223
+ // Make sure our paint options are set properly, in particular raster layer visibility
224
+ this.setPaint(this.__layerOptions);
225
225
  }
226
226
 
227
- setColour(options)
228
- //================
227
+ setPaint(options)
228
+ //===============
229
229
  {
230
230
  const coloured = !('colour' in options) || options.colour;
231
231
  for (const layer of this.__layers) {
@@ -250,7 +250,7 @@ export class LayerManager
250
250
  colour: true,
251
251
  outline: true,
252
252
  sckan: 'valid'
253
- });;
253
+ });
254
254
  const backgroundLayer = new style.BackgroundLayer();
255
255
  if ('background' in flatmap.options) {
256
256
  this.__map.addLayer(backgroundLayer.style(flatmap.options.background));
@@ -305,19 +305,19 @@ export class LayerManager
305
305
  this.__layerOptions.activeRasterLayer = enable;
306
306
  for (const mapLayer of this.__mapLayers.values()) {
307
307
  if (mapLayer.id !== RASTER_LAYERS_ID) {
308
- mapLayer.setColour(this.__layerOptions);
308
+ mapLayer.setPaint(this.__layerOptions);
309
309
  }
310
310
  }
311
311
  }
312
312
  }
313
313
  }
314
314
 
315
- setColour(options={})
316
- //===================
315
+ setPaint(options={})
316
+ //==================
317
317
  {
318
318
  this.__layerOptions = utils.setDefaults(options, this.__layerOptions);
319
319
  for (const mapLayer of this.__mapLayers.values()) {
320
- mapLayer.setColour(this.__layerOptions);
320
+ mapLayer.setPaint(this.__layerOptions);
321
321
  }
322
322
  }
323
323
 
@@ -329,6 +329,31 @@ export class LayerManager
329
329
  mapLayer.setFilter(this.__layerOptions);
330
330
  }
331
331
  }
332
+
333
+ enableSckanPath(sckanState, enable=true)
334
+ //======================================
335
+ {
336
+ const currentState = this.__layerOptions.sckan;
337
+ const validEnabled = ['valid', 'all'].indexOf(currentState) >= 0;
338
+ const invalidEnabled = ['invalid', 'all'].indexOf(currentState) >= 0;
339
+ let newState = sckanState.toLowerCase();
340
+ if (newState === 'valid') {
341
+ if (enable && !validEnabled) {
342
+ newState = invalidEnabled ? 'all' : 'valid';
343
+ } else if (!enable && validEnabled) {
344
+ newState = invalidEnabled ? 'invalid' : 'none';
345
+ }
346
+ } else if (newState === 'invalid') {
347
+ if (enable && !invalidEnabled) {
348
+ newState = validEnabled ? 'all' : 'invalid';
349
+ } else if (!enable && invalidEnabled) {
350
+ newState = validEnabled ? 'valid' : 'none';
351
+ }
352
+ }
353
+ if (newState !== this.__layerOptions.sckan) {
354
+ this.setFilter({sckan: newState});
355
+ }
356
+ }
332
357
  }
333
358
 
334
359
  //==============================================================================
package/src/pathways.js CHANGED
@@ -26,19 +26,20 @@ export const PATHWAYS_LAYER = 'pathways';
26
26
 
27
27
  //==============================================================================
28
28
 
29
- export const PATH_TYPES = [
29
+ const PATH_TYPES = [
30
30
  { type: "cns", label: "CNS", colour: "#9B1FC1"},
31
31
  { type: "intracardiac", label: "Local circuit neuron", colour: "#F19E38"},
32
32
  { type: "para-pre", label: "Parasympathetic pre-ganglionic", colour: "#3F8F4A"},
33
- { type: "para-post", label: "Parasympathetic post-ganglionic", colour: "#3F8F4A"},
33
+ { type: "para-post", label: "Parasympathetic post-ganglionic", colour: "#3F8F4A", dashed: true},
34
34
  { type: "sensory", label: "Sensory (afferent) neuron", colour: "#2A62F6"},
35
35
  { type: "somatic", label: "Somatic lower motor", colour: "#98561D"},
36
36
  { type: "symp-pre", label: "Sympathetic pre-ganglionic", colour: "#EA3423"},
37
- { type: "symp-post", label: "Sympathetic post-ganglionic", colour: "#EA3423"},
37
+ { type: "symp-post", label: "Sympathetic post-ganglionic", colour: "#EA3423", dashed: true},
38
38
  { type: "other", label: "Other neuron type", colour: "#888"},
39
39
  { type: "arterial", label: "Arterial blood vessel", colour: "#F00"},
40
40
  { type: "venous", label: "Venous blood vessel", colour: "#2F6EBA"},
41
- { type: "centreline", label: "Nerve centrelines", colour: "#2F6EBA", enabled: false}
41
+ { type: "centreline", label: "Nerve centrelines", colour: "#CCC", enabled: false},
42
+ { type: "error", label: "Paths with errors or warnings", colour: "#FF0"}
42
43
  ];
43
44
 
44
45
  export const PATH_STYLE_RULES =
@@ -142,16 +143,28 @@ export class Pathways
142
143
  this.__typePaths['other'].push(...paths);
143
144
  }
144
145
  }
146
+ // Nerve centrelines are a special case with their own controls
147
+ this.__haveCentrelines = false;
145
148
  }
146
149
 
147
- get pathTypes()
148
- //=============
150
+ get haveCentrelines()
151
+ //===================
152
+ {
153
+ return this.__haveCentrelines;
154
+ }
155
+
156
+ pathTypes()
157
+ //=========
149
158
  {
150
159
  const pathTypes = [];
151
160
  for (const pathType of PATH_TYPES) {
152
161
  if (pathType.type in this.__typePaths
153
162
  && this.__typePaths[pathType.type].length > 0) {
154
- pathTypes.push(pathType);
163
+ if (pathType.type === 'centreline') {
164
+ this.__haveCentrelines = true;
165
+ } else {
166
+ pathTypes.push(pathType);
167
+ }
155
168
  }
156
169
  }
157
170
  return pathTypes;
package/src/styling.js CHANGED
@@ -162,7 +162,8 @@ export class FeatureFillLayer extends VectorStyleLayer
162
162
  'filter': [
163
163
  'all',
164
164
  ['==', '$type', 'Polygon'],
165
- ['!=', 'models', 'UBERON:0013702']
165
+ ['!=', 'models', 'UBERON:0013702'],
166
+ ['!has', 'node']
166
167
  ],
167
168
  'layout': {
168
169
  'fill-sort-key': ['get', 'scale']
@@ -244,7 +245,8 @@ export class FeatureBorderLayer extends VectorStyleLayer
244
245
  'type': 'line',
245
246
  'filter': [
246
247
  'all',
247
- ['==', '$type', 'Polygon']
248
+ ['==', '$type', 'Polygon'],
249
+ ['!has', 'node']
248
250
  ],
249
251
  'paint': this.paintStyle(options)
250
252
  };
@@ -287,8 +289,8 @@ export class FeatureLineLayer extends VectorStyleLayer
287
289
  'line-color': [
288
290
  'case',
289
291
  ['boolean', ['feature-state', 'selected'], false], '#0F0',
290
- ['has', 'colour'], ['get', 'colour'],
291
292
  ['boolean', ['feature-state', 'active'], false], coloured ? '#888' : '#CCC',
293
+ ['has', 'colour'], ['get', 'colour'],
292
294
  ['==', ['get', 'type'], 'network'], '#AFA202',
293
295
  options.authoring ? '#C44' : '#444'
294
296
  ],
@@ -415,7 +417,6 @@ export class PathLineLayer extends VectorStyleLayer
415
417
  ['boolean', ['feature-state', 'selected'], false], '#0F0',
416
418
  ['boolean', ['feature-state', 'hidden'], false], '#CCC',
417
419
  ['==', ['get', 'type'], 'bezier'], 'red',
418
- ['has', 'error'], '#FFFE0E',
419
420
  ['==', ['get', 'kind'], 'unknown'], '#888',
420
421
  ...PATH_STYLE_RULES,
421
422
  '#888'
@@ -424,6 +425,7 @@ export class PathLineLayer extends VectorStyleLayer
424
425
  'case',
425
426
  ['boolean', ['feature-state', 'hidden'], false], 0.05,
426
427
  ['==', ['get', 'type'], 'bezier'], 1.0,
428
+ ['==', ['get', 'kind'], 'error'], 1.0,
427
429
  ['boolean', ['get', 'invisible'], false], 0.001,
428
430
  ['boolean', ['feature-state', 'selected'], false], 1.0,
429
431
  ['boolean', ['feature-state', 'active'], false], 1.0,
@@ -434,7 +436,7 @@ export class PathLineLayer extends VectorStyleLayer
434
436
  'width', ["*", [
435
437
  'case',
436
438
  ['==', ['get', 'type'], 'bezier'], 0.1,
437
- ['has', 'error'], 1,
439
+ ['==', ['get', 'kind'], 'error'], 1,
438
440
  ['==', ['get', 'kind'], 'unknown'], 1,
439
441
  ['boolean', ['get', 'invisible'], false], 0.1,
440
442
  ['boolean', ['feature-state', 'selected'], false], 0.6,
@@ -568,6 +570,76 @@ export class CentrelineTrackLayer extends CentrelineLayer
568
570
 
569
571
  //==============================================================================
570
572
 
573
+ export class CentrelineNodeFillLayer extends VectorStyleLayer
574
+ {
575
+ constructor(id, sourceLayer)
576
+ {
577
+ super(id, 'node-fill', sourceLayer);
578
+ }
579
+
580
+ paintStyle(options={}, changes=false)
581
+ {
582
+ const showNodes = options.showCentrelines || false;
583
+ const paintStyle = {
584
+ 'fill-color': '#AFA202',
585
+ 'fill-opacity': showNodes ? 0.7 : 0.01
586
+ }
587
+ return super.changedPaintStyle(paintStyle, changes);
588
+ }
589
+
590
+ style(options)
591
+ {
592
+ return {
593
+ ...super.style(),
594
+ 'type': 'fill',
595
+ 'filter': [
596
+ 'all',
597
+ ['==', '$type', 'Polygon'],
598
+ ['has', 'node']
599
+ ],
600
+ 'layout': {
601
+ 'fill-sort-key': ['get', 'scale']
602
+ },
603
+ 'paint': this.paintStyle(options)
604
+ };
605
+ }
606
+ }
607
+
608
+ export class CentrelineNodeBorderLayer extends VectorStyleLayer
609
+ {
610
+ constructor(id, sourceLayer)
611
+ {
612
+ super(id, 'node-border', sourceLayer);
613
+ }
614
+
615
+ paintStyle(options={}, changes=false)
616
+ {
617
+ const showNodes = options.showCentrelines || false;
618
+ const paintStyle = {
619
+ 'line-color': '#AFA202',
620
+ 'line-opacity': showNodes ? 0.7 : 0.01,
621
+ 'line-width': 0.5
622
+ }
623
+ return super.changedPaintStyle(paintStyle, changes);
624
+ }
625
+
626
+ style(options)
627
+ {
628
+ return {
629
+ ...super.style(),
630
+ 'type': 'line',
631
+ 'filter': [
632
+ 'all',
633
+ ['==', '$type', 'Polygon'],
634
+ ['has', 'node']
635
+ ],
636
+ 'paint': this.paintStyle(options)
637
+ };
638
+ }
639
+ }
640
+
641
+ //==============================================================================
642
+
571
643
  export class FeatureNerveLayer extends VectorStyleLayer
572
644
  {
573
645
  constructor(id, sourceLayer)
@@ -202,40 +202,13 @@ li.flatmap-contextmenu-item:hover {
202
202
  label[for=path-all-paths] {
203
203
  font-weight: bold;
204
204
  }
205
- .nerve-cns {
206
- background: #9B1FC1;
207
- }
208
- .nerve-intracardiac {
209
- background: #F19E38;
210
- }
211
- .nerve-other {
212
- background: #888;
213
- }
214
- .nerve-para-pre {
215
- background: #3F8F4A;
216
- }
217
- .nerve-para-post {
218
- background: repeating-linear-gradient(to right,#3F8F4A 0,#3F8F4A 6px,transparent 6px,transparent 9px)
219
- }
220
- .nerve-sensory {
221
- background: #2A62F6;
222
- }
223
- .nerve-somatic {
224
- background: #98561D;
225
- }
226
- .nerve-symp-pre {
227
- background: #EA3423;
228
- }
229
- .nerve-symp-post {
230
- background: repeating-linear-gradient(to right,#EA3423 0,#EA3423 6px,transparent 6px,transparent 9px)
231
- }
232
205
 
233
206
  /* Layer control */
234
207
 
235
- #flatmap-layer-control {
208
+ .flatmap-control {
236
209
  text-align: right;
237
210
  }
238
- .flatmap-layer-grid {
211
+ .flatmap-control-grid {
239
212
  margin-top: 10px;
240
213
  display: grid;
241
214
  grid-template-columns: 3.8fr 0.2fr;
@@ -250,7 +223,7 @@ label[for=path-all-paths] {
250
223
  padding: 4px;
251
224
  opacity: 0.8;
252
225
  }
253
- .flatmap-layer-grid input {
226
+ .flatmap-control-grid input {
254
227
  height: 1.1em;
255
228
  }
256
229
  label[for=layer-all-layers] {