@abi-software/flatmap-viewer 2.3.3-b.3 → 2.3.4

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.3.3-b.3``
41
+ * ``npm install @abi-software/flatmap-viewer@2.3.4``
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.3.3-b.3",
3
+ "version": "2.3.4",
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/annotation.js CHANGED
@@ -508,7 +508,7 @@ export class Annotator
508
508
  border: '2px solid #080',
509
509
  borderRadius: '.5rem',
510
510
  panelSize: 'auto auto',
511
- position: 'left-top',
511
+ position: 'left-top 50 70',
512
512
  content: panelContent,
513
513
  data: features[0].properties,
514
514
  closeOnEscape: true,
@@ -588,7 +588,7 @@ export class Annotator
588
588
  border: '2px solid #080',
589
589
  borderRadius: '.5rem',
590
590
  panelSize: '725px auto',
591
- position: 'left-top',
591
+ position: 'left-top 50 70',
592
592
  data: {
593
593
  flatmap: this.__flatmap
594
594
  },
@@ -27,6 +27,7 @@ export const displayedProperties = [
27
27
  'class',
28
28
  'fc-class',
29
29
  'fc-kind',
30
+ 'name',
30
31
  ...indexedProperties
31
32
  ];
32
33
 
@@ -33,7 +33,7 @@ import '../static/css/flatmap-viewer.css';
33
33
 
34
34
  //==============================================================================
35
35
 
36
- import {MapServer} from './mapserver.js';
36
+ import {MapServer, loadJSON} from './mapserver.js';
37
37
  import {SearchIndex} from './search.js';
38
38
  import {UserInteractions} from './interactions.js';
39
39
 
@@ -77,11 +77,15 @@ class FlatMap
77
77
  this.__datasetToFeatureIds = new Map();
78
78
  this.__modelToFeatureIds = new Map();
79
79
  this.__mapSourceToFeatureIds = new Map();
80
+ this.__annIdToFeatureId = new Map();
80
81
 
81
82
  for (const [featureId, annotation] of Object.entries(mapDescription.annotations)) {
82
83
  this.__addAnnotation(featureId, annotation);
83
84
  this.__searchIndex.indexMetadata(featureId, annotation);
84
85
  }
86
+ if (this.options.annotator) {
87
+ this.__addAnnotatedComments();
88
+ }
85
89
 
86
90
  // Set base of source URLs in map's style
87
91
 
@@ -207,6 +211,25 @@ class FlatMap
207
211
  });
208
212
  }
209
213
 
214
+ async __addAnnotatedComments()
215
+ //============================
216
+ {
217
+ const url = this.makeServerUrl('', 'annotator/')
218
+ const annotatedFeatures = await loadJSON(url);
219
+ for (const annotatedId of annotatedFeatures) {
220
+ const featureId = this.__annIdToFeatureId.get(annotatedId);
221
+ if (featureId) {
222
+ const url = this.makeServerUrl(annotatedId, 'annotator/')
223
+ const annotations = await loadJSON(url);
224
+ for (const annotation of annotations) { // In order of most recent to oldest
225
+ if ('rdfs:comment' in annotation) {
226
+ this.__searchIndex.indexText(featureId, annotation['rdfs:comment']);
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+
210
233
  async setupUserInteractions_()
211
234
  //============================
212
235
  {
@@ -498,6 +521,7 @@ class FlatMap
498
521
  this.__updateFeatureIdMap('dataset', this.__datasetToFeatureIds, ann);
499
522
  this.__updateFeatureIdMap('models', this.__modelToFeatureIds, ann);
500
523
  this.__updateFeatureIdMap('source', this.__mapSourceToFeatureIds, ann);
524
+ this.__annIdToFeatureId.set(ann.id, featureId);
501
525
  }
502
526
 
503
527
  modelFeatureIds(anatomicalId)
@@ -1122,13 +1146,12 @@ class FlatMap
1122
1146
  }
1123
1147
 
1124
1148
  /**
1125
- * Zoom map to features.
1149
+ * Select features and zoom the map to them.
1126
1150
  *
1127
- * @param {Array.<string>} externalIds An array of anaotomical terms identifing features
1151
+ * @param {Array.<string>} featureIds An array of feature identifiers
1128
1152
  * @param {Object} [options]
1129
- * @param {boolean} [options.select=true] Select the features zoomed to
1130
- * @param {boolean} [options.highlight=false] Highlight the features zoomed to
1131
- * @param {number} [options.padding=100] Padding around the composite bounding box
1153
+ * @param {boolean} [options.noZoomIn=false] Don't zoom in (although zoom out as necessary)
1154
+ * @param {number} [options.padding=10] Padding in pixels around the composite bounding box
1132
1155
  */
1133
1156
  zoomToFeatures(externalIds, options=null)
1134
1157
  //=======================================
@@ -402,19 +402,36 @@ export class UserInteractions
402
402
  selectFeature(featureId, dim=true)
403
403
  //================================
404
404
  {
405
- featureId = +featureId; // Ensure numeric
406
- if (this._selectedFeatureIds.size === 0) {
407
- this._layerManager.setPaint({...this.__colourOptions, dimmed: dim});
405
+ const ann = this._flatmap.annotation(featureId);
406
+ if ('sckan' in ann) {
407
+ const sckanState = this._layerManager.sckanState;
408
+ if (sckanState === 'none'
409
+ || sckanState === 'valid' && !ann.sckan
410
+ || sckanState === 'invalid' && ann.sckan) {
411
+ return false;
412
+ }
408
413
  }
414
+ featureId = +featureId; // Ensure numeric
415
+ let result = false;
416
+ const noSelection = (this._selectedFeatureIds.size === 0);
409
417
  if (this._selectedFeatureIds.has(featureId)) {
410
418
  this._selectedFeatureIds.set(featureId, this._selectedFeatureIds.get(featureId) + 1);
419
+ result = true;
411
420
  } else {
412
421
  const feature = this.mapFeature(featureId);
413
422
  if (feature !== undefined) {
414
- this._map.setFeatureState(feature, { 'selected': true });
415
- this._selectedFeatureIds.set(featureId, 1);
423
+ const state = this._map.getFeatureState(feature);
424
+ if (state !== undefined && (!('hidden' in state) || !state.hidden)) {
425
+ this._map.setFeatureState(feature, { 'selected': true });
426
+ this._selectedFeatureIds.set(featureId, 1);
427
+ result = true;
428
+ }
416
429
  }
417
430
  }
431
+ if (result && noSelection) {
432
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: dim});
433
+ }
434
+ return result;
418
435
  }
419
436
 
420
437
  unselectFeature(featureId)
@@ -566,10 +583,11 @@ export class UserInteractions
566
583
  for (const featureId of featureIds) {
567
584
  const annotation = this._flatmap.annotation(featureId);
568
585
  if (annotation) {
569
- this.selectFeature(featureId);
570
- if ('type' in annotation && annotation.type.startsWith('line')) {
571
- for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
572
- this.selectFeature(pathFeatureId);
586
+ if (this.selectFeature(featureId)) {
587
+ if ('type' in annotation && annotation.type.startsWith('line')) {
588
+ for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
589
+ this.selectFeature(pathFeatureId);
590
+ }
573
591
  }
574
592
  }
575
593
  }
@@ -580,16 +598,15 @@ export class UserInteractions
580
598
  showSearchResults(featureIds)
581
599
  //===========================
582
600
  {
583
- this.zoomToFeatures(featureIds, {highlight: true, noZoomIn: true});
601
+ this.unselectFeatures();
602
+ this.zoomToFeatures(featureIds, {noZoomIn: true});
584
603
  }
585
604
 
586
605
  /**
587
- * Zoom map to features.
606
+ * Select features and zoom the map to them.
588
607
  *
589
608
  * @param {Array.<string>} featureIds An array of feature identifiers
590
609
  * @param {Object} [options]
591
- * @param {boolean} [options.select=true] Select the features zoomed to
592
- * @param {boolean} [options.highlight=false] Highlight the features zoomed to
593
610
  * @param {boolean} [options.noZoomIn=false] Don't zoom in (although zoom out as necessary)
594
611
  * @param {number} [options.padding=10] Padding in pixels around the composite bounding box
595
612
  */
@@ -597,16 +614,11 @@ export class UserInteractions
597
614
  //======================================
598
615
  {
599
616
  options = utils.setDefaults(options, {
600
- select: true,
601
- highlight:
602
- false, noZoomIn:
603
- false, padding:10
617
+ noZoomIn: false,
618
+ padding: 10
604
619
  });
605
- const select = (options.select === true);
606
- const highlight = (options.highlight === true);
607
620
  if (featureIds.length) {
608
- this.unhighlightFeatures_();
609
- if (select) this.unselectFeatures();
621
+ this.unselectFeatures();
610
622
  let bbox = null;
611
623
  if (options.noZoomIn) {
612
624
  const bounds = this._map.getBounds().toArray();
@@ -615,21 +627,15 @@ export class UserInteractions
615
627
  for (const featureId of featureIds) {
616
628
  const annotation = this._flatmap.annotation(featureId);
617
629
  if (annotation) {
618
- if (select) {
619
- this.selectFeature(featureId);
620
- } else if (highlight) {
621
- this.highlightFeature_(featureId);
622
- }
623
- bbox = expandBounds(bbox, annotation.bounds);
624
- if ('type' in annotation && annotation.type.startsWith('line')) {
625
- for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
626
- if (select) {
627
- this.selectFeature(pathFeatureId);
628
- } else if (highlight) {
629
- this.highlightFeature_(pathFeatureId);
630
+ if (this.selectFeature(featureId)) {
631
+ bbox = expandBounds(bbox, annotation.bounds);
632
+ if ('type' in annotation && annotation.type.startsWith('line')) {
633
+ for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
634
+ if (this.selectFeature(pathFeatureId)) {
635
+ const pathAnnotation = this._flatmap.annotation(pathFeatureId)
636
+ bbox = expandBounds(bbox, pathAnnotation.bounds);
637
+ }
630
638
  }
631
- const pathAnnotation = this._flatmap.annotation(pathFeatureId)
632
- bbox = expandBounds(bbox, pathAnnotation.bounds);
633
639
  }
634
640
  }
635
641
  }
@@ -652,15 +658,24 @@ export class UserInteractions
652
658
  // Remove any existing popup
653
659
 
654
660
  if (this._currentPopup) {
661
+ if (options && options.preserveSelection) {
662
+ this._currentPopup.options.preserveSelection = options.preserveSelection;
663
+ }
655
664
  this._currentPopup.remove();
656
665
  }
657
666
 
658
- if (!(options && options.preserveSelection)) {
659
- // Highlight the feature
667
+ // Clear selection if we are not preserving it
668
+
669
+ if (options && options.preserveSelection) {
670
+ delete options.preserveSelection; // Don't pass to onClose()
671
+ } else { // via the popup's options
660
672
  this.unselectFeatures();
661
- this.selectFeature(featureId);
662
673
  }
663
674
 
675
+ // Select the feature
676
+
677
+ this.selectFeature(featureId);
678
+
664
679
  // Find the pop-up's postion
665
680
 
666
681
  let location = null;
@@ -680,7 +695,7 @@ export class UserInteractions
680
695
  }
681
696
  this.setModal_();
682
697
  this._currentPopup = new maplibre.Popup(options).addTo(this._map);
683
- this._currentPopup.on('close', this.__clearPopup.bind(this));
698
+ this._currentPopup.on('close', this.__onCloseCurrentPopup.bind(this));
684
699
  this._currentPopup.setLngLat(location);
685
700
  if (typeof content === 'object') {
686
701
  this._currentPopup.setDOMContent(content);
@@ -690,11 +705,16 @@ export class UserInteractions
690
705
  }
691
706
  }
692
707
 
693
- __clearPopup()
694
- //============
708
+ __onCloseCurrentPopup()
709
+ //=====================
695
710
  {
696
- this.__clearModal();
697
- this.unselectFeatures();
711
+ if (this._currentPopup) {
712
+ this.__clearModal();
713
+ if (!(this._currentPopup.options && this._currentPopup.options.preserveSelection)) {
714
+ this.unselectFeatures();
715
+ }
716
+ this._currentPopup = null;
717
+ }
698
718
  }
699
719
 
700
720
  removeTooltip_()
@@ -918,26 +938,34 @@ export class UserInteractions
918
938
  //=======================================
919
939
  {
920
940
  // Show a tooltip
921
- if (html !== '') {
922
- this._tooltip = new maplibre.Popup({
923
- closeButton: false,
924
- closeOnClick: false,
925
- maxWidth: 'none',
926
- className: 'flatmap-tooltip-popup'
927
- });
941
+ if (html !== '' || this._flatmap.options.showId && feature !== null) {
942
+ let header = '';
928
943
  if (this._flatmap.options.showPosition) {
929
944
  const pt = turf.point(lngLat.toArray());
930
945
  const gps = turfProjection.toMercator(pt);
931
946
  const coords = gps.geometry.coordinates;
932
- const header = (feature === null)
947
+ header = (feature === null)
933
948
  ? JSON.stringify(coords)
934
- : `${JSON.stringify(coords)} (${feature.id} ${feature.properties['id']})`;
949
+ : `${JSON.stringify(coords)} (${feature.id})`;
950
+ }
951
+ if (this._flatmap.options.showId && feature !== null && 'id' in feature.properties) {
952
+ header = `${header} ${feature.properties.id}`;
953
+ }
954
+ if (header !== '') {
935
955
  html = `<span>${header}</span><br/>${html}`;
936
956
  }
937
- this._tooltip
938
- .setLngLat(lngLat)
939
- .setHTML(html)
940
- .addTo(this._map);
957
+ if (html !== '') {
958
+ this._tooltip = new maplibre.Popup({
959
+ closeButton: false,
960
+ closeOnClick: false,
961
+ maxWidth: 'none',
962
+ className: 'flatmap-tooltip-popup'
963
+ });
964
+ this._tooltip
965
+ .setLngLat(lngLat)
966
+ .setHTML(html)
967
+ .addTo(this._map);
968
+ }
941
969
  }
942
970
  }
943
971
 
package/src/layers.js CHANGED
@@ -298,6 +298,12 @@ export class LayerManager
298
298
  return layers;
299
299
  }
300
300
 
301
+ get sckanState()
302
+ //==============
303
+ {
304
+ return this.__layerOptions.sckan;
305
+ }
306
+
301
307
  activate(layerId, enable=true)
302
308
  //============================
303
309
  {
package/src/main.js CHANGED
@@ -59,6 +59,7 @@ export async function standaloneViewer(map_endpoint=null, options={})
59
59
  background: defaultBackground,
60
60
  debug: false,
61
61
  minimap: false,
62
+ showId: true,
62
63
  showPosition: false,
63
64
  standalone: true,
64
65
  annotator: true
package/src/mapserver.js CHANGED
@@ -22,6 +22,24 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
+ export async function loadJSON(url)
26
+ //=================================
27
+ {
28
+ const response = await fetch(url, {
29
+ method: 'GET',
30
+ headers: {
31
+ "Accept": "application/json; charset=utf-8",
32
+ "Cache-Control": "no-store"
33
+ }
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`Cannot access ${url}`);
37
+ }
38
+ return response.json();
39
+ }
40
+
41
+ //==============================================================================
42
+
25
43
  export class MapServer
26
44
  {
27
45
  constructor(url)
@@ -39,18 +57,7 @@ export class MapServer
39
57
  async loadJSON(relativePath)
40
58
  //==========================
41
59
  {
42
- const url = this.url(relativePath);
43
- const response = await fetch(url, {
44
- method: 'GET',
45
- headers: {
46
- "Accept": "application/json; charset=utf-8",
47
- "Cache-Control": "no-store"
48
- }
49
- });
50
- if (!response.ok) {
51
- throw new Error(`Cannot access ${url}`);
52
- }
53
- return response.json();
60
+ return loadJSON(this.url(relativePath));
54
61
  }
55
62
  }
56
63
 
package/src/pathways.js CHANGED
@@ -42,7 +42,7 @@ const PATH_TYPES = [
42
42
  { type: "arterial", label: "Arterial blood vessel", colour: "#F00", enabled: false},
43
43
  { type: "venous", label: "Venous blood vessel", colour: "#2F6EBA", enabled: false},
44
44
  { type: "centreline", label: "Nerve centrelines", colour: "#CCC", enabled: false},
45
- { type: "error", label: "Paths with errors or warnings", colour: "#FF0"}
45
+ { type: "error", label: "Paths with errors or warnings", colour: "#FF0", enabled: false}
46
46
  ];
47
47
 
48
48
  export const PATH_STYLE_RULES =
@@ -65,12 +65,11 @@ export class PathManager
65
65
  }
66
66
  }
67
67
  }
68
- this.__pathModelPaths = {}; // pathModelId: [pathIds]
69
- this.__pathToPathModel = {};
70
-
71
- this.__paths = {};
72
- const pathLines = {}; // pathId: [lineIds]
73
- const pathNerves = {}; // pathId: [nerveIds]
68
+ this.__pathModelPaths = {}; // pathModelId: [pathIds]
69
+ this.__pathToPathModel = {}; // pathId: pathModelId
70
+ this.__paths = {}; // pathId: path
71
+ const pathLines = {}; // pathId: [lineIds]
72
+ const pathNerves = {}; // pathId: [nerveIds]
74
73
  if ('paths' in flatmap.pathways) {
75
74
  for (const [pathId, path] of Object.entries(flatmap.pathways.paths)) {
76
75
  pathLines[pathId] = path.lines;
package/src/search.js CHANGED
@@ -56,18 +56,19 @@ export class SearchIndex
56
56
  if (prop in metadata) {
57
57
  const text = metadata[prop];
58
58
  if (!textSeen.includes(text)) {
59
- this.addTerm_(featureId, text);
59
+ this.indexText(featureId, text);
60
60
  textSeen.push(text);
61
61
  }
62
62
  }
63
63
  }
64
64
  }
65
65
 
66
- addTerm_(featureId, text)
67
- //=======================
66
+ indexText(featureId, text)
67
+ //========================
68
68
  {
69
69
  text = text.replace(new RegExp('<br/>', 'g'), ' ')
70
- .replace('\n', ' ');
70
+ .replace(new RegExp('\n', 'g'), ' ')
71
+ ;
71
72
  if (text) {
72
73
  this._searchEngine.add({
73
74
  id: this._featureIds.length,
package/src/styling.js CHANGED
@@ -31,7 +31,7 @@ import {PATH_STYLE_RULES} from './pathways.js';
31
31
  //==============================================================================
32
32
 
33
33
  const COLOUR_ACTIVE = 'blue';
34
- const COLOUR_ANNOTATED = '#0F0';
34
+ const COLOUR_ANNOTATED = '#C8F';
35
35
  const COLOUR_SELECTED = '#0F0';
36
36
  const COLOUR_HIDDEN = '#D8D8D8';
37
37
 
@@ -418,15 +418,18 @@ export class AnnotatedPathLayer extends VectorStyleLayer
418
418
 
419
419
  paintStyle(options={}, changes=false)
420
420
  {
421
+ const dimmed = 'dimmed' in options && options.dimmed;
421
422
  const exclude = 'excludeAnnotated' in options && options.excludeAnnotated;
422
423
  const paintStyle = {
423
424
  'line-color': COLOUR_ANNOTATED,
424
425
  'line-dasharray': [5, 0.5, 3, 0.5],
425
426
  'line-opacity': [
426
427
  'case',
428
+ ['boolean', ['feature-state', 'active'], false], 0.8,
429
+ ['boolean', ['feature-state', 'selected'], false], 0.8,
427
430
  ['boolean', ['feature-state', 'hidden'], false], 0.05,
428
431
  ['boolean', ['feature-state', 'annotated'], false],
429
- (exclude ? 0.05 : 0.8),
432
+ ((exclude || dimmed) ? 0.05 : 0.8),
430
433
  0.6
431
434
  ],
432
435
  'line-width': [
@@ -435,7 +438,11 @@ export class AnnotatedPathLayer extends VectorStyleLayer
435
438
  ['case',
436
439
  ['boolean', ['feature-state', 'hidden'], false], 0.0,
437
440
  ['boolean', ['feature-state', 'annotated'], false],
438
- exclude ? 0.0 : (['*', 1.2, ['case', ['has', 'stroke-width'], ['get', 'stroke-width'], 1.0]]),
441
+ exclude ? 0.0 : (['*', 1.1, ['case',
442
+ ['has', 'stroke-width'], ['get', 'stroke-width'],
443
+ ['boolean', ['feature-state', 'active'], false], 1.1,
444
+ ['boolean', ['feature-state', 'active'], false], 1.1,
445
+ 1.0]]),
439
446
  0.0
440
447
  ],
441
448
  STROKE_INTERPOLATION
@@ -446,7 +453,6 @@ export class AnnotatedPathLayer extends VectorStyleLayer
446
453
 
447
454
  style(options)
448
455
  {
449
- const dimmed = 'dimmed' in options && options.dimmed;
450
456
  return {
451
457
  ...super.style(),
452
458
  'type': 'line',