@abi-software/flatmap-viewer 2.4.0-a.2 → 2.4.1-b.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.0-a.2``
41
+ * ``npm install @abi-software/flatmap-viewer@2.4.1-b.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.0-a.2",
3
+ "version": "2.4.1-b.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",
@@ -26,7 +26,7 @@
26
26
  "bezier-js": "^6.1.0",
27
27
  "html-es6cape": "^2.0.2",
28
28
  "jspanel4": "^4.16.1",
29
- "maplibre-gl": ">=2.4.0",
29
+ "maplibre-gl": ">=3.0.0",
30
30
  "minisearch": "^2.2.1",
31
31
  "polylabel": "^1.1.0"
32
32
  },
@@ -22,7 +22,7 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
- import maplibre from 'maplibre-gl';
25
+ import maplibregl from 'maplibre-gl';
26
26
 
27
27
  //==============================================================================
28
28
 
@@ -64,7 +64,7 @@ export class ContextMenu
64
64
  this._flatmap = flatmap;
65
65
  this._map = flatmap.map;
66
66
  this._closeCallback = closeCallback;
67
- this._popup = new maplibre.Popup({
67
+ this._popup = new maplibregl.Popup({
68
68
  closeButton: true,
69
69
  closeOnClick: true,
70
70
  className: 'flatmap-contextmenu-popup',
@@ -45,7 +45,7 @@ limitations under the License.
45
45
 
46
46
  //==============================================================================
47
47
 
48
- import maplibre from 'maplibre-gl';
48
+ import maplibregl from 'maplibre-gl';
49
49
 
50
50
  //==============================================================================
51
51
 
@@ -146,7 +146,7 @@ export class MinimapControl
146
146
 
147
147
  // Create the actual minimap
148
148
 
149
- this._miniMap = new maplibre.Map({
149
+ this._miniMap = new maplibregl.Map({
150
150
  attributionControl: false,
151
151
  container: container,
152
152
  style: map.getStyle(),
@@ -32,7 +32,7 @@ export class SystemsControl extends Control
32
32
  }
33
33
 
34
34
  _addControlDetails()
35
- //===============
35
+ //==================
36
36
  {
37
37
  let lines = 0;
38
38
  let enabled = 0;
@@ -0,0 +1,73 @@
1
+ /******************************************************************************
2
+
3
+ Flatmap viewer and annotation tool
4
+
5
+ Copyright (c) 2019 - 2023 David Brooks
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+
19
+ **/
20
+ //==============================================================================
21
+
22
+ import { Control } from './controls';
23
+
24
+ //==============================================================================
25
+
26
+ export class TaxonsControl extends Control
27
+ {
28
+ constructor(flatmap)
29
+ {
30
+ super(flatmap, 'taxon', 'taxons');
31
+ this.__taxonIds = flatmap.taxonIdentifiers.sort();
32
+ }
33
+
34
+ _addControlDetails()
35
+ //==================
36
+ {
37
+ let lines = 0;
38
+ let enabled = 0;
39
+ for (const taxonId of this.__taxonIds) {
40
+ const input = this._addControlLine(`${this.__prefix}${taxonId}`, taxonId);
41
+ input.checked = true;
42
+ enabled += 1;
43
+ lines += 1;
44
+ }
45
+ return {
46
+ enabled: enabled,
47
+ total: lines
48
+ };
49
+ }
50
+
51
+ _enableAll(enable)
52
+ //================
53
+ {
54
+ for (const taxonId of this.__taxonIds) {
55
+ const checkbox = document.getElementById(`${this.__prefix}${taxonId}`);
56
+ if (checkbox) {
57
+ checkbox.checked = enable;
58
+ this.__flatmap.enableConnectivityByTaxonIds(taxonId, enable);
59
+ }
60
+ }
61
+ }
62
+
63
+ __enableControl(id, enable)
64
+ //=========================
65
+ {
66
+ for (const taxonId of this.__taxonIds) {
67
+ if (id === taxonId) {
68
+ this.__flatmap.enableConnectivityByTaxonIds(taxonId, enable);
69
+ }
70
+ }
71
+ }
72
+
73
+ }
@@ -49,6 +49,15 @@ const MAP_MAKER_SEPARATE_LAYERS_VERSION = 1.4;
49
49
 
50
50
  //==============================================================================
51
51
 
52
+ /**
53
+ * The taxon identifier used when none has been given.
54
+ *
55
+ * @type {string}
56
+ */
57
+ export const UNCLASSIFIED_TAXON_ID = 'NCBITaxon:2787823'; // unclassified entries
58
+
59
+ //==============================================================================
60
+
52
61
  /**
53
62
  * Maps are not created directly but instead are created and loaded by
54
63
  * :meth:`LoadMap` of :class:`MapManager`.
@@ -79,6 +88,7 @@ class FlatMap
79
88
  this.__modelToFeatureIds = new Map();
80
89
  this.__mapSourceToFeatureIds = new Map();
81
90
  this.__annIdToFeatureId = new Map();
91
+ this.__taxonToFeatureIds = new Map();
82
92
 
83
93
  for (const [featureId, annotation] of Object.entries(mapDescription.annotations)) {
84
94
  this.__addAnnotation(featureId, annotation);
@@ -331,6 +341,26 @@ class FlatMap
331
341
  }
332
342
  }
333
343
 
344
+ /**
345
+ * Show or hide connectivity features observed in particular species.
346
+ *
347
+ * @param {string | Array.<string>} taxonId(s) A single taxon identifier
348
+ * or an array of identifiers.
349
+ * @param {boolean} enable Show or hide connectivity paths and features.
350
+ * Defaults to ``true`` (show)
351
+ */
352
+ enableConnectivityByTaxonIds(taxonIds, enable=true)
353
+ //=================================================
354
+ {
355
+ if (this._userInteractions !== null) {
356
+ if (Array.isArray(taxonIds)) {
357
+ this._userInteractions.enableConnectivityByTaxonIds(taxonIds, enable);
358
+ } else {
359
+ this._userInteractions.enableConnectivityByTaxonIds([taxonIds], enable);
360
+ }
361
+ }
362
+ }
363
+
334
364
  /**
335
365
  * Hide or show centrelines and nodes.
336
366
  *
@@ -511,17 +541,31 @@ class FlatMap
511
541
  }
512
542
  }
513
543
 
514
- __updateFeatureIdMap(property, featureIdMap, annotation)
544
+ __updateFeatureIdMapEntry(propertyId, featureIdMap, featureId)
515
545
  //======================================================
516
546
  {
517
- if (property in annotation) {
518
- const id = utils.normaliseId(annotation[property]);
519
- const featureIds = featureIdMap.get(id);
520
- if (featureIds) {
521
- featureIds.push(annotation.featureId);
547
+ const featureIds = featureIdMap.get(propertyId);
548
+ if (featureIds) {
549
+ featureIds.push(featureId);
550
+ } else {
551
+ featureIdMap.set(propertyId, [featureId]);
552
+ }
553
+ }
554
+
555
+ __updateFeatureIdMap(property, featureIdMap, annotation, missingId=null)
556
+ //======================================================================
557
+ {
558
+ if (property in annotation && annotation[property].length) {
559
+ const propertyId = annotation[property];
560
+ if (Array.isArray(propertyId)) {
561
+ for (const id of propertyId) {
562
+ this.__updateFeatureIdMapEntry(id, featureIdMap, annotation.featureId);
563
+ }
522
564
  } else {
523
- featureIdMap.set(id, [annotation.featureId]);
565
+ this.__updateFeatureIdMapEntry(propertyId, featureIdMap, annotation.featureId);
524
566
  }
567
+ } else if (missingId !== null) {
568
+ this.__updateFeatureIdMapEntry(missingId, featureIdMap, annotation.featureId);
525
569
  }
526
570
  }
527
571
 
@@ -533,6 +577,7 @@ class FlatMap
533
577
  this.__updateFeatureIdMap('dataset', this.__datasetToFeatureIds, ann);
534
578
  this.__updateFeatureIdMap('models', this.__modelToFeatureIds, ann);
535
579
  this.__updateFeatureIdMap('source', this.__mapSourceToFeatureIds, ann);
580
+ this.__updateFeatureIdMap('taxons', this.__taxonToFeatureIds, ann, UNCLASSIFIED_TAXON_ID);
536
581
  this.__annIdToFeatureId.set(ann.id, featureId);
537
582
  }
538
583
 
@@ -610,6 +655,17 @@ class FlatMap
610
655
  return [...this.__modelToFeatureIds.keys()]
611
656
  }
612
657
 
658
+ /**
659
+ * The taxon identifiers of species which the map's connectivity has been observed in.
660
+ *
661
+ * @type {string|Array.<string>}
662
+ */
663
+ get taxonIdentifiers()
664
+ //====================
665
+ {
666
+ return [...this.__taxonToFeatureIds.keys()]
667
+ }
668
+
613
669
  /**
614
670
  * Datasets associated with the map.
615
671
  *
@@ -817,7 +873,7 @@ class FlatMap
817
873
  /**
818
874
  * Get a list of the flatmap's layers.
819
875
  *
820
- * @return {Array.Object.<{id: string, description: string, enabled: boolean}>} An array with layer details
876
+ * @return {Array.<{id: string, description: string, enabled: boolean}>} An array with layer details
821
877
  */
822
878
  getLayers()
823
879
  //=========
@@ -845,7 +901,7 @@ class FlatMap
845
901
  /**
846
902
  * Get a list of a FC flatmap's systems.
847
903
  *
848
- * @return {Array.Object.<{id: string, name: string, colour: string, enabled: boolean}>} An array with system details
904
+ * @return {Array.<{id: string, name: string, colour: string, enabled: boolean}>} An array with system details
849
905
  */
850
906
  getSystems()
851
907
  //==========
@@ -970,7 +1026,7 @@ class FlatMap
970
1026
  'models',
971
1027
  'nodeId',
972
1028
  'source',
973
- 'taxon',
1029
+ 'taxons',
974
1030
  'hyperlinks'
975
1031
  ];
976
1032
  const jsonProperties = [
@@ -22,7 +22,7 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
- import maplibre from 'maplibre-gl';
25
+ import maplibregl from 'maplibre-gl';
26
26
 
27
27
  import {default as turfArea} from '@turf/area';
28
28
  import {default as turfBBox} from '@turf/bbox';
@@ -45,6 +45,7 @@ import {AnnotatedControl, BackgroundControl, LayerControl, NerveControl,
45
45
  import {PathControl} from './controls/paths';
46
46
  import {SearchControl} from './controls/search';
47
47
  import {SystemsControl} from './controls/systems';
48
+ import {TaxonsControl} from './controls/taxons';
48
49
 
49
50
  import * as utils from './utils';
50
51
 
@@ -80,12 +81,13 @@ function bounds(feature)
80
81
 
81
82
  //==============================================================================
82
83
 
83
- function expandBounds(bbox1, bbox2)
84
- //=================================
84
+ function expandBounds(bbox1, bbox2, padding)
85
+ //==========================================
85
86
  {
86
- return (bbox1 === null) ? bbox2
87
- : [Math.min(bbox1[0], bbox2[0]), Math.min(bbox1[1], bbox2[1]),
88
- Math.max(bbox1[2], bbox2[2]), Math.max(bbox1[3], bbox2[3])
87
+ return (bbox1 === null) ? [bbox2[0]-padding.lng, bbox2[1]-padding.lat,
88
+ bbox2[2]+padding.lng, bbox2[3]+padding.lat]
89
+ : [Math.min(bbox1[0], bbox2[0]-padding.lng), Math.min(bbox1[1], bbox2[1]-padding.lat),
90
+ Math.max(bbox1[2], bbox2[2]+padding.lng), Math.max(bbox1[3], bbox2[3]+padding.lat)
89
91
  ];
90
92
  }
91
93
 
@@ -158,11 +160,12 @@ export class UserInteractions
158
160
  }
159
161
 
160
162
  // Note features that are FC systems
161
-
162
163
  this.__systemsManager = new SystemsManager(this._flatmap, this, featuresEnabled);
163
164
 
164
- // Add various controls when running standalone
165
+ // All taxons of connectivity paths are enabled by default
166
+ this.__enabledConnectivityTaxons = new Set(this._flatmap.taxonIdentifiers);
165
167
 
168
+ // Add various controls when running standalone
166
169
  if (flatmap.options.standalone) {
167
170
  // Add a control to search annotations if option set
168
171
  this._map.addControl(new SearchControl(flatmap));
@@ -185,11 +188,14 @@ export class UserInteractions
185
188
  this._map.addControl(new NerveControl(flatmap, this._layerManager, {showCentrelines: false}));
186
189
  }
187
190
 
188
- // SCKAN path and SYSTEMS controls for FC maps
189
191
  if (flatmap.options.style === 'functional') {
192
+ // SCKAN path and SYSTEMS controls for FC maps
190
193
  this._map.addControl(new SystemsControl(flatmap, this.__systemsManager.systems));
191
194
  this._map.addControl(new SCKANControl(flatmap, flatmap.options.layerOptions));
192
195
  this._map.addControl(new AnnotatedControl(this, flatmap.options.layerOptions));
196
+ } else {
197
+ // Connectivity taxon control for AC maps
198
+ this._map.addControl(new TaxonsControl(flatmap));
193
199
  }
194
200
  }
195
201
 
@@ -615,7 +621,7 @@ export class UserInteractions
615
621
  {
616
622
  options = utils.setDefaults(options, {
617
623
  noZoomIn: false,
618
- padding: 10
624
+ padding: 10 // pixels
619
625
  });
620
626
  if (featureIds.length) {
621
627
  this.unselectFeatures();
@@ -624,16 +630,20 @@ export class UserInteractions
624
630
  const bounds = this._map.getBounds().toArray();
625
631
  bbox = [...bounds[0], ...bounds[1]];
626
632
  }
633
+ // Convert pixel padding to LngLat and apply it to a feature's bounds
634
+ const padding = this._map.unproject({x: options.padding, y: options.padding});
635
+ padding.lng -= bbox[0];
636
+ padding.lat = bbox[3] - padding.lat;
627
637
  for (const featureId of featureIds) {
628
638
  const annotation = this._flatmap.annotation(featureId);
629
639
  if (annotation) {
630
640
  if (this.selectFeature(featureId)) {
631
- bbox = expandBounds(bbox, annotation.bounds);
641
+ bbox = expandBounds(bbox, annotation.bounds, padding);
632
642
  if ('type' in annotation && annotation.type.startsWith('line')) {
633
643
  for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
634
644
  if (this.selectFeature(pathFeatureId)) {
635
645
  const pathAnnotation = this._flatmap.annotation(pathFeatureId)
636
- bbox = expandBounds(bbox, pathAnnotation.bounds);
646
+ bbox = expandBounds(bbox, pathAnnotation.bounds, padding);
637
647
  }
638
648
  }
639
649
  }
@@ -642,7 +652,7 @@ export class UserInteractions
642
652
  }
643
653
  if (bbox !== null) {
644
654
  this._map.fitBounds(bbox, {
645
- padding: options.padding,
655
+ padding: 0,
646
656
  animate: false
647
657
  });
648
658
  }
@@ -694,7 +704,7 @@ export class UserInteractions
694
704
  this._map.panTo(location);
695
705
  }
696
706
  this.setModal_();
697
- this._currentPopup = new maplibre.Popup(options).addTo(this._map);
707
+ this._currentPopup = new maplibregl.Popup(options).addTo(this._map);
698
708
  this._currentPopup.on('close', this.__onCloseCurrentPopup.bind(this));
699
709
  this._currentPopup.setLngLat(location);
700
710
  if (typeof content === 'object') {
@@ -955,7 +965,7 @@ export class UserInteractions
955
965
  html = `<span>${header}</span><br/>${html}`;
956
966
  }
957
967
  if (html !== '') {
958
- this._tooltip = new maplibre.Popup({
968
+ this._tooltip = new maplibregl.Popup({
959
969
  closeButton: false,
960
970
  closeOnClick: false,
961
971
  maxWidth: 'none',
@@ -1127,6 +1137,21 @@ export class UserInteractions
1127
1137
  this._layerManager.enableSckanPaths(sckanState, enable);
1128
1138
  }
1129
1139
 
1140
+ enableConnectivityByTaxonIds(taxonIds, enable=true)
1141
+ //=================================================
1142
+ {
1143
+ if (enable) {
1144
+ for (const taxonId of taxonIds) {
1145
+ this.__enabledConnectivityTaxons.add(taxonId);
1146
+ }
1147
+ } else {
1148
+ for (const taxonId of taxonIds) {
1149
+ this.__enabledConnectivityTaxons.delete(taxonId);
1150
+ }
1151
+ }
1152
+ this._layerManager.setFilter({taxons: [...this.__enabledConnectivityTaxons.values()]});
1153
+ }
1154
+
1130
1155
  excludeAnnotated(exclude=false)
1131
1156
  //=============================
1132
1157
  {
@@ -1190,8 +1215,8 @@ export class UserInteractions
1190
1215
  // order to apply a scale transform we need to create marker icons
1191
1216
  // inside the marker container <div>.
1192
1217
  const colour = options.colour || '#005974';
1193
- const markerHTML = options.element ? new maplibre.Marker({element: options.element})
1194
- : new maplibre.Marker({color: colour});
1218
+ const markerHTML = options.element ? new maplibregl.Marker({element: options.element})
1219
+ : new maplibregl.Marker({color: colour});
1195
1220
 
1196
1221
  const markerElement = document.createElement('div');
1197
1222
  const markerIcon = document.createElement('div');
@@ -1201,9 +1226,9 @@ export class UserInteractions
1201
1226
  markerElement.appendChild(markerIcon);
1202
1227
 
1203
1228
  const markerPosition = this.__markerPosition(featureId, annotation);
1204
- const marker = new maplibre.Marker(markerElement)
1205
- .setLngLat(markerPosition)
1206
- .addTo(this._map);
1229
+ const marker = new maplibregl.Marker(markerElement)
1230
+ .setLngLat(markerPosition)
1231
+ .addTo(this._map);
1207
1232
  markerElement.addEventListener('mouseenter',
1208
1233
  this.markerMouseEvent_.bind(this, marker, anatomicalId));
1209
1234
  markerElement.addEventListener('mousemove',
@@ -1345,7 +1370,7 @@ export class UserInteractions
1345
1370
 
1346
1371
  element.addEventListener('click', e => this.__clearActiveMarker());
1347
1372
 
1348
- this._tooltip = new maplibre.Popup({
1373
+ this._tooltip = new maplibregl.Popup({
1349
1374
  closeButton: false,
1350
1375
  closeOnClick: false,
1351
1376
  maxWidth: 'none',
package/src/styling.js CHANGED
@@ -26,7 +26,8 @@ export const VECTOR_TILES_SOURCE = 'vector-tiles';
26
26
 
27
27
  //==============================================================================
28
28
 
29
- import {PATH_STYLE_RULES} from './pathways.js';
29
+ import {UNCLASSIFIED_TAXON_ID} from './flatmap-viewer';
30
+ import {PATH_STYLE_RULES} from './pathways';
30
31
 
31
32
  //==============================================================================
32
33
 
@@ -374,24 +375,24 @@ function sckanFilter(options)
374
375
  : options.sckan.toLowerCase();
375
376
  const sckanFilter =
376
377
  sckanState == 'none' ? [
377
- ['!has', 'sckan']
378
+ ['!', ['has', 'sckan']]
378
379
  ] :
379
380
  sckanState == 'valid' ? [[
380
381
  'any',
381
- ['!has', 'sckan'],
382
+ ['!', ['has', 'sckan']],
382
383
  [
383
384
  'all',
384
385
  ['has', 'sckan'],
385
- ['==', 'sckan', true]
386
+ ['==', ['get', 'sckan'], true]
386
387
  ]
387
388
  ]] :
388
389
  sckanState == 'invalid' ? [[
389
390
  'any',
390
- ['!has', 'sckan'],
391
+ ['!', ['has', 'sckan']],
391
392
  [
392
393
  'all',
393
394
  ['has', 'sckan'],
394
- ['!=', 'sckan', true]
395
+ ['!=', ['get', 'sckan'], true]
395
396
  ]
396
397
  ]] :
397
398
  [ ];
@@ -411,7 +412,6 @@ export class AnnotatedPathLayer extends VectorStyleLayer
411
412
  {
412
413
  return [
413
414
  'all',
414
- ['==', '$type', 'LineString'],
415
415
  ...sckanFilter(options)
416
416
  ];
417
417
  }
@@ -481,22 +481,38 @@ export class PathLineLayer extends VectorStyleLayer
481
481
  makeFilter(options={})
482
482
  {
483
483
  const sckan_filter = sckanFilter(options);
484
+ let taxonFilter = [];
485
+ if ('taxons' in options) {
486
+ if (options.taxons.length) {
487
+ taxonFilter.push('any');
488
+ for (const taxon of options.taxons) {
489
+ if (taxon !== UNCLASSIFIED_TAXON_ID) {
490
+ taxonFilter.push(['in', taxon, ['get', 'taxons']]);
491
+ } else {
492
+ taxonFilter.push(['case', ['has', 'taxons'], false, true]);
493
+ }
494
+ }
495
+ taxonFilter = [taxonFilter];
496
+ } else {
497
+ taxonFilter.push(false);
498
+ }
499
+ }
484
500
 
485
501
  return this.__dashed ? [
486
502
  'all',
487
- ['==', '$type', 'LineString'],
488
- ['==', 'type', `line-dash`],
489
- ...sckan_filter
503
+ ['==', ['get', 'type'], 'line-dash'],
504
+ ...sckan_filter,
505
+ ...taxonFilter
490
506
  ] : [
491
507
  'all',
492
- ['==', '$type', 'LineString'],
493
508
  [
494
509
  'any',
495
- ['==', 'type', 'bezier'],
510
+ ['==', ['get', 'type'], 'bezier'],
496
511
  [
497
512
  'all',
498
- ['==', 'type', `line`],
499
- ...sckan_filter
513
+ ['==', ['get', 'type'], 'line'],
514
+ ...sckan_filter,
515
+ ...taxonFilter
500
516
  ]
501
517
  ]
502
518
  ];
package/src/utils.js CHANGED
@@ -22,6 +22,10 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
+ const NO_NORMALISATION = ['http', 'https', 'urn', 'NCBITaxon'];
26
+
27
+ //==============================================================================
28
+
25
29
  export class List extends Array {
26
30
  constructor(iterable=null) {
27
31
  super();
@@ -101,7 +105,7 @@ export function normaliseId(id)
101
105
  }
102
106
  const parts = id.split(':')
103
107
  const lastPart = parts[parts.length - 1]
104
- if (['http', 'https', 'urn'].includes(parts[0]) || !'0123456789'.includes(lastPart[0])) {
108
+ if (NO_NORMALISATION.includes(parts[0]) || !'0123456789'.includes(lastPart[0])) {
105
109
  return id;
106
110
  }
107
111
  parts[parts.length - 1] = lastPart.padStart(8, '0');