@abi-software/flatmap-viewer 2.3.0-a.1 → 2.3.0-a.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,16 +33,18 @@ import polylabel from 'polylabel';
33
33
 
34
34
  //==============================================================================
35
35
 
36
- import {ContextMenu} from './contextmenu.js';
37
- import {displayedProperties} from './info.js';
38
- import {InfoControl} from './info.js';
39
- import {LayerManager} from './layers.js';
40
- import {PATHWAYS_LAYER, Pathways} from './pathways.js';
41
- import {PathControl} from './controls.js';
42
- import {SearchControl} from './search.js';
43
- import {VECTOR_TILES_SOURCE} from './styling.js';
44
-
45
- import * as utils from './utils.js';
36
+ import {Annotator} from './annotation';
37
+ import {displayedProperties, InfoControl} from './info';
38
+ import {LayerManager} from './layers';
39
+ import {PATHWAYS_LAYER, Pathways} from './pathways';
40
+ import {BackgroundControl, LayerControl, NerveControl,
41
+ PathControl, SCKANControl} from './controls';
42
+ import {SearchControl} from './search';
43
+ import {VECTOR_TILES_SOURCE} from './styling';
44
+ import {SystemsControl, SystemsManager} from './systems';
45
+
46
+ import * as pathways from './pathways';
47
+ import * as utils from './utils';
46
48
 
47
49
  //==============================================================================
48
50
 
@@ -114,75 +116,90 @@ export class UserInteractions
114
116
  this.__activeMarker = null;
115
117
  this.__lastMarkerId = 900000;
116
118
  this.__markerIdByMarker = new Map();
119
+ this.__markerIdByFeatureId = new Map();
117
120
  this.__annotationByMarkerId = new Map();
118
121
 
119
122
  // Where to put labels and popups on a feature
120
- this.__centralPositions = new Map();
121
-
122
- // MapLibre dynamically sets a transform on marker elements so in
123
- // order to apply a scale transform we need to create marker icons
124
- // inside the marker container <div>.
125
- this._defaultMarkerHTML = new maplibre.Marker().getElement().innerHTML;
126
- this._simulationMarkerHTML = new maplibre.Marker({color: '#005974'}).getElement().innerHTML;
123
+ this.__markerPositions = new Map();
127
124
 
128
125
  // Fit the map to its initial position
129
126
 
130
127
  flatmap.setInitialPosition();
131
128
 
132
- // Add a control to search annotations if option set
133
-
134
- if (flatmap.options.searchable) {
135
- this._map.addControl(new SearchControl(flatmap));
136
- }
137
-
138
- // Show information about features
129
+ // Add and manage our layers
139
130
 
140
- if (flatmap.options.featureInfo || flatmap.options.searchable) {
141
- this._infoControl = new InfoControl(flatmap);
142
- if (flatmap.options.featureInfo) {
143
- this._map.addControl(this._infoControl);
144
- }
145
- }
131
+ this._layerManager = new LayerManager(flatmap);
146
132
 
147
- // Neural pathways which are either controlled externally
148
- // or by our local controls
133
+ // Path visibility is either controlled externally or by a local control
149
134
 
150
135
  this._pathways = new Pathways(flatmap);
151
136
 
152
- if (flatmap.options.pathControls) {
153
- // Add controls to manage our pathways
154
- this._map.addControl(new PathControl(flatmap, this._pathways));
155
- }
137
+ // The path types in this map
138
+ const mapPathTypes = this._pathways.pathTypes();
156
139
 
157
- // Manage our layers
158
-
159
- this._layerManager = new LayerManager(flatmap);
140
+ // Disable paths that are not initially shown
141
+ for (const path of mapPathTypes) {
142
+ if ('enabled' in path && !path.enabled) {
143
+ this.enablePath(path.type, false);
144
+ }
145
+ }
160
146
 
161
- // Flag features that have annotations
162
- // Also flag those features that are models of something
147
+ // Flag features that have annotations and note which are FC systems
163
148
 
149
+ this.__systems = new Map();
164
150
  for (const [id, ann] of flatmap.annotations) {
165
151
  const feature = this.mapFeature_(id);
166
152
  if (feature !== undefined) {
167
153
  this._map.setFeatureState(feature, { 'annotated': true });
168
154
  }
155
+ if (ann['fc-class'] === 'fc-class:System') {
156
+ if (this.__systems.has(ann.name)) {
157
+ this.__systems.get(ann.name).featureIds.push(ann.featureId)
158
+ } else {
159
+ this.__systems.set(ann.name, {
160
+ id: ann.name.replaceAll(' ', '_'),
161
+ colour: ann.colour,
162
+ featureIds: [ ann.featureId ]
163
+ });
164
+ }
165
+ }
169
166
  }
170
167
 
171
- // Display a context menu on right-click
168
+ // Add various controls when running standalone
169
+
170
+ if (flatmap.options.standalone) {
171
+ // Add a control to search annotations if option set
172
+ this._map.addControl(new SearchControl(flatmap));
173
+
174
+ // Show information about features
175
+ this._infoControl = new InfoControl(flatmap);
176
+ this._map.addControl(this._infoControl);
177
+
178
+ // Control background colour (NB. this depends on having map layers created)
179
+ this._map.addControl(new BackgroundControl(flatmap));
172
180
 
173
- this._lastContextTime = 0;
174
- this._contextMenu = new ContextMenu(flatmap, this.__clearModal.bind(this));
175
- this._map.on('contextmenu', this.contextMenuEvent_.bind(this));
181
+ // Add a control to manage our paths
182
+ this._map.addControl(new PathControl(flatmap, mapPathTypes));
176
183
 
177
- // Display a context menu with a touch longer than 0.5 second
184
+ // Add a control to manage our layers
185
+ this._map.addControl(new LayerControl(flatmap, this._layerManager));
178
186
 
179
- this._lastTouchTime = 0;
180
- this._map.on('touchstart', (e) => { this._lastTouchTime = Date.now(); });
181
- this._map.on('touchend', (e) => {
182
- if (Date.now() > (this._lastTouchTime + 500)) {
183
- this.contextMenuEvent_(e);
187
+ // Add a control for nerve centrelines if they are present
188
+ if (this._pathways.haveCentrelines) {
189
+ this._map.addControl(new NerveControl(flatmap, this._layerManager, {showCentrelines: false}));
190
+ this.enableCentrelines(false);
184
191
  }
185
- });
192
+
193
+ // SCKAN path and SYSTEMS controls for FC maps
194
+ if (flatmap.options.style === 'functional') {
195
+ this._map.addControl(new SystemsControl(flatmap, this.__systems));
196
+ this._map.addControl(new SCKANControl(flatmap, flatmap.options.layerOptions));
197
+ }
198
+ }
199
+
200
+ // Add annotation capabilities
201
+
202
+ this.__annotator = new Annotator(flatmap);
186
203
 
187
204
  // Handle mouse events
188
205
 
@@ -198,6 +215,12 @@ export class UserInteractions
198
215
  this.__pan_zoom_enabled = false;
199
216
  }
200
217
 
218
+ get pathways()
219
+ //============
220
+ {
221
+ return this._pathways;
222
+ }
223
+
201
224
  getState()
202
225
  //========
203
226
  {
@@ -206,7 +229,7 @@ export class UserInteractions
206
229
  return {
207
230
  center: this._map.getCenter().toArray(),
208
231
  zoom: this._map.getZoom(),
209
- layers: this.activeLayerNames
232
+ layers: this.layers
210
233
  };
211
234
  }
212
235
 
@@ -231,17 +254,83 @@ export class UserInteractions
231
254
  }
232
255
  }
233
256
 
234
- setColour(options)
235
- //================
257
+ setPaint(options)
258
+ //===============
236
259
  {
237
260
  this.__colourOptions = options;
238
- this._layerManager.setColour(options);
261
+ this._layerManager.setPaint(options);
239
262
  }
240
263
 
241
- get activeLayerNames()
242
- //====================
264
+ getLayers()
265
+ //=========
266
+ {
267
+ return this._layerManager.layers;
268
+ }
269
+
270
+ enableLayer(layerId, enable=true)
271
+ //===============================
272
+ {
273
+ this._layerManager.activate(layerId, enable);
274
+ }
275
+
276
+ getSystems()
277
+ //==========
278
+ {
279
+ const systems = [];
280
+ for (const system of this.__systems.values()) {
281
+ systems.push({
282
+ name: system.name,
283
+ colour: system.colour,
284
+ });
285
+ }
286
+ return systems;
287
+ }
288
+
289
+ enableSystem(systemName, enable=true)
290
+ //===================================
291
+ {
292
+ if (this.__systems.has(systemName)) {
293
+ for (const featureId of this.__systems.get(systemName).featureIds) {
294
+ this.__enableFeatureWithChildren(featureId, enable);
295
+ }
296
+ }
297
+ }
298
+
299
+ __enableFeatureWithChildren(featureId, enable=true)
300
+ //=================================================
301
+ {
302
+ const feature = this.mapFeature_(featureId);
303
+ if (feature !== undefined) {
304
+ this.__enableFeature(feature, enable);
305
+ for (const childFeatureId of feature.children) {
306
+ this.__enableFeatureWithChildren(childFeatureId, enable);
307
+ }
308
+ }
309
+ }
310
+
311
+ __enableFeatureMarker(featureId, enable=true)
312
+ //===========================================
313
+ {
314
+ const markerId = this.__markerIdByFeatureId.get(+featureId);
315
+ if (markerId !== undefined) {
316
+ const markerDiv = document.getElementById(`marker-${markerId}`);
317
+ if (markerDiv) {
318
+ markerDiv.style.visibility = enable ? 'visible' : 'hidden';
319
+ }
320
+ }
321
+ }
322
+
323
+ __enableFeature(feature, enable=true)
324
+ //===================================
243
325
  {
244
- return this._layerManager.activeLayerNames;
326
+ if (feature !== undefined) {
327
+ if (enable) {
328
+ this._map.removeFeatureState(feature, 'hidden');
329
+ } else {
330
+ this._map.setFeatureState(feature, { 'hidden': true });
331
+ }
332
+ this.__enableFeatureMarker(feature.id, enable);
333
+ }
245
334
  }
246
335
 
247
336
  mapFeature_(featureId)
@@ -252,11 +341,13 @@ export class UserInteractions
252
341
  return {
253
342
  id: featureId,
254
343
  source: VECTOR_TILES_SOURCE,
255
- sourceLayer: this._flatmap.options.separateLayers
344
+ sourceLayer: (this._flatmap.options.separateLayers
256
345
  ? `${ann['layer']}_${ann['tile-layer']}`
257
- : ann['tile-layer']
346
+ : ann['tile-layer']).replaceAll('/', '_'),
347
+ children: ann.children || []
258
348
  };
259
349
  }
350
+ return undefined;
260
351
  }
261
352
 
262
353
  featureSelected_(featureId)
@@ -270,7 +361,7 @@ export class UserInteractions
270
361
  {
271
362
  featureId = +featureId; // Ensure numeric
272
363
  if (this._selectedFeatureIds.size === 0) {
273
- this._layerManager.setColour({...this.__colourOptions, dimmed: dim});
364
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: dim});
274
365
  }
275
366
  if (this._selectedFeatureIds.has(featureId)) {
276
367
  this._selectedFeatureIds.set(featureId, this._selectedFeatureIds.get(featureId) + 1);
@@ -300,7 +391,7 @@ export class UserInteractions
300
391
  }
301
392
  }
302
393
  if (this._selectedFeatureIds.size === 0) {
303
- this._layerManager.setColour({...this.__colourOptions, dimmed: false});
394
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: false});
304
395
  }
305
396
  }
306
397
 
@@ -314,20 +405,7 @@ export class UserInteractions
314
405
  }
315
406
  }
316
407
  this._selectedFeatureIds.clear();
317
- this._layerManager.setColour({...this.__colourOptions, dimmed: false});
318
- }
319
-
320
- activeFeaturesAtEvent_(event)
321
- //===========================
322
- {
323
- // Get the features covering the event's point that are in the active layers
324
-
325
- return this._map.queryRenderedFeatures(event.point).filter(f => {
326
- return (this.__enabledFeature(f)
327
- && this.activeLayerNames.indexOf(f.sourceLayer) >= 0)
328
- && ('featureId' in f.properties);
329
- }
330
- );
408
+ this._layerManager.setPaint({...this.__colourOptions, dimmed: false});
331
409
  }
332
410
 
333
411
  __activateFeature(feature)
@@ -381,45 +459,6 @@ export class UserInteractions
381
459
  return smallestFeature;
382
460
  }
383
461
 
384
- contextMenuEvent_(event)
385
- //======================
386
- {
387
- event.preventDefault();
388
-
389
- // Chrome on Android sends both touch and contextmenu events
390
- // so ignore duplicate
391
-
392
- if (Date.now() < (this._lastContextTime + 100)) {
393
- return;
394
- }
395
- this._lastContextTime = Date.now();
396
-
397
- if (this._activeFeatures.length > 0) {
398
- const feature = this._activeFeatures[0];
399
-
400
- // Remove any tooltip
401
- this.removeTooltip_();
402
-
403
- const featureId = feature.id;
404
- if (this._pathways.isNode(featureId)) {
405
- const items = [
406
- {
407
- featureId: featureId,
408
- prompt: 'Show paths',
409
- action: this.enablePaths_.bind(this, true)
410
- },
411
- {
412
- featureId: featureId,
413
- prompt: 'Hide paths',
414
- action: this.enablePaths_.bind(this, false)
415
- }
416
- ];
417
- this.setModal_();
418
- this._contextMenu.show(event.lngLat, items, feature.properties.label);
419
- }
420
- }
421
- }
422
-
423
462
  setModal_(event)
424
463
  //==============
425
464
  {
@@ -515,7 +554,12 @@ export class UserInteractions
515
554
  zoomToFeatures(featureIds, options=null)
516
555
  //======================================
517
556
  {
518
- options = utils.setDefaultOptions(options, {select: true, highlight: false, noZoomIn: false, padding:10});
557
+ options = utils.setDefaults(options, {
558
+ select: true,
559
+ highlight:
560
+ false, noZoomIn:
561
+ false, padding:10
562
+ });
519
563
  const select = (options.select === true);
520
564
  const highlight = (options.highlight === true);
521
565
  if (featureIds.length) {
@@ -583,7 +627,7 @@ export class UserInteractions
583
627
  location = this.__lastClickLngLat;
584
628
  } else {
585
629
  // Position popup at the feature's 'centre'
586
- location = this.__centralPosition(featureId, ann);
630
+ location = this.__markerPosition(featureId, ann);
587
631
  }
588
632
 
589
633
  // Make sure the feature is on screen
@@ -625,9 +669,13 @@ export class UserInteractions
625
669
  const tooltips = [];
626
670
  for (const lineFeature of lineFeatures) {
627
671
  const properties = lineFeature.properties;
628
- if ('label' in properties
629
- && (!('tooltip' in properties) || properties.tooltip)
630
- && !('labelled' in properties)) {
672
+ if ('error' in properties) {
673
+ tooltips.push(`<div class="feature-error">Error: ${properties.error}</div>`)
674
+ }
675
+ if ('warning' in properties) {
676
+ tooltips.push(`<div class="feature-error">Warning: ${properties.warning}</div>`)
677
+ }
678
+ if ('label' in properties && (!('tooltip' in properties) || properties.tooltip)) {
631
679
  let tooltip = '';
632
680
  const label = properties.label;
633
681
  const cleanLabel = (label.substr(0, 1).toUpperCase() + label.substr(1)).replaceAll("\n", "<br/>");
@@ -636,32 +684,37 @@ export class UserInteractions
636
684
  }
637
685
  }
638
686
  }
639
- if (tooltips.length === 0) {
640
- return '';
641
- }
642
- return `<div class='flatmap-feature-label'>${tooltips.join('<hr/>')}</div>`;
687
+ return (tooltips.length === 0) ? ''
688
+ : `<div class='flatmap-feature-label'>${tooltips.join('<hr/>')}</div>`;
643
689
  }
644
690
 
645
691
  tooltipHtml_(properties, forceLabel=false)
646
692
  //========================================
647
693
  {
694
+ const tooltip = [];
695
+ if ('error' in properties) {
696
+ tooltip.push(`<div class="feature-error">Error: ${properties.error}</div>`)
697
+ }
698
+ if ('warning' in properties) {
699
+ tooltip.push(`<div class="feature-error">Warning: ${properties.warning}</div>`)
700
+ }
648
701
  if (('label' in properties || 'hyperlink' in properties)
649
- && (forceLabel || !('tooltip' in properties) || properties.tooltip)
650
- && !('labelled' in properties)) {
651
- let tooltip = '';
652
- if ('label' in properties) {
653
- const label = properties.label;
654
- tooltip = (label.substr(0, 1).toUpperCase() + label.substr(1)).replaceAll("\n", "<br/>");
655
- } else {
656
- tooltip = properties.hyperlink
657
- }
702
+ && (forceLabel || !('tooltip' in properties) || properties.tooltip)) {
703
+ const label = ('label' in properties) ? (properties.label.substr(0, 1).toUpperCase()
704
+ + properties.label.substr(1)).replaceAll("\n", "<br/>")
705
+ : '';
658
706
  if ('hyperlink' in properties) {
659
- return `<div class='flatmap-feature-label'><a href='{properties.hyperlink}'>${tooltip}</a></div>`;
707
+ if (label === '') {
708
+ tooltip.push(`<a href='${properties.hyperlink}'>${properties.hyperlink}</a>`);
709
+ } else {
710
+ tooltip.push(`<a href='${properties.hyperlink}'>${label}</a></div>`);
711
+ }
660
712
  } else {
661
- return `<div class='flatmap-feature-label'>${tooltip}</div>`;
713
+ tooltip.push(label);
662
714
  }
663
715
  }
664
- return '';
716
+ return (tooltip.length === 0) ? ''
717
+ : `<div class='flatmap-feature-label'>${tooltip.join('<hr/>')}</div>`;
665
718
  }
666
719
 
667
720
  __featureEvent(type, feature)
@@ -841,59 +894,83 @@ export class UserInteractions
841
894
  selectionEvent_(event, feature)
842
895
  //=============================
843
896
  {
844
- const multipleSelect = event.ctrlKey || event.metaKey;
845
- if (!multipleSelect) {
846
- this.__unselectFeatures();
847
- }
848
897
  if (feature !== undefined) {
849
- const featureId = feature.id;
850
- const selecting = !this.featureSelected_(featureId);
851
- if ('properties' in feature
852
- && 'type' in feature.properties
853
- && feature.properties.type.startsWith('line')) {
898
+ const clickedFeatureId = feature.id;
899
+ const dim = !('properties' in feature
900
+ && 'kind' in feature.properties
901
+ && ['cell-type', 'scaffold', 'tissue'].indexOf(feature.properties.kind) >= 0);
902
+ if (!(event.ctrlKey || event.metaKey)) {
903
+ let selecting = true;
904
+ for (const featureId of this._selectedFeatureIds.keys()) {
905
+ if (featureId === clickedFeatureId) {
906
+ selecting = false;
907
+ break;
908
+ }
909
+ }
910
+ this.__unselectFeatures();
911
+ if (selecting) {
912
+ for (const feature of this._activeFeatures) {
913
+ this.selectFeature_(feature.id, dim);
914
+ }
915
+ }
916
+ } else {
917
+ const clickedSelected = this.featureSelected_(clickedFeatureId);
854
918
  for (const feature of this._activeFeatures) {
855
- const featureId = feature.id;
856
- if (selecting) {
857
- this.selectFeature_(featureId);
919
+ if (clickedSelected) {
920
+ this.unselectFeature_(feature.id);
858
921
  } else {
859
- this.unselectFeature_(featureId);
922
+ this.selectFeature_(feature.id, dim);
860
923
  }
861
924
  }
862
- } else if (selecting) {
863
- const dim = !('properties' in feature
864
- && 'kind' in feature.properties
865
- && ['cell-type', 'scaffold', 'tissue'].indexOf(feature.properties.kind) >= 0);
866
- this.selectFeature_(featureId, dim);
867
- } else {
868
- this.unselectFeature_(featureId);
869
925
  }
870
926
  }
871
927
  }
872
928
 
929
+ __annotationEvent(feature)
930
+ //========================
931
+ {
932
+ event.preventDefault();
933
+
934
+ // Remove any tooltip
935
+ this.removeTooltip_();
936
+
937
+ // Select the feature
938
+ this.selectFeature_(feature.id);
939
+
940
+ // Don't respond to mouse events while the dialog is open
941
+ this.setModal_();
942
+
943
+ // The annotation dialog...
944
+ this.__annotator.annotate(feature, e => {
945
+ this.__unselectFeatures();
946
+ this.__clearModal();
947
+ });
948
+ }
949
+
873
950
  clickEvent_(event)
874
951
  //================
875
952
  {
953
+ if (this._modal) {
954
+ return;
955
+ }
956
+
876
957
  this.clearActiveMarker_();
877
958
  const clickedFeatures = this._map.queryRenderedFeatures(event.point)
878
959
  .filter(feature => this.__enabledFeature(feature));
879
960
  if (clickedFeatures.length == 0){
961
+ this.__unselectFeatures();
880
962
  return;
881
963
  }
882
964
  const clickedFeature = clickedFeatures[0];
883
965
  const originalEvent = event.originalEvent;
884
- if (clickedFeature === undefined || this._activeFeatures.length === 1) {
885
- this.selectionEvent_(originalEvent, clickedFeature);
886
- } else if (this._activeFeatures.length > 1) {
887
- const multipleSelect = originalEvent.ctrlKey || originalEvent.metaKey;
888
- if (!multipleSelect) {
889
- this.__unselectFeatures();
890
- }
891
- for (const feature of this._activeFeatures) {
892
- this.selectFeature_(feature.id);
893
- }
966
+ if (originalEvent.altKey) {
967
+ this.__annotationEvent(clickedFeature);
968
+ return;
894
969
  }
970
+
971
+ this.selectionEvent_(originalEvent, clickedFeature);
895
972
  if (this._modal) {
896
- // Remove tooltip, reset active features, etc
973
+ // Remove tooltip, reset active features, etc
897
974
  this.__resetFeatureDisplay();
898
975
  this.__unselectFeatures();
899
976
  this.__clearModal();
@@ -932,7 +1009,6 @@ export class UserInteractions
932
1009
  enablePaths_(enable, event)
933
1010
  //=========================
934
1011
  {
935
- this._contextMenu.hide();
936
1012
  const nodeId = event.target.getAttribute('featureId');
937
1013
  this.enablePathFeatures_(enable, this._pathways.pathFeatureIds(nodeId));
938
1014
  this.__clearModal();
@@ -957,6 +1033,7 @@ export class UserInteractions
957
1033
  togglePaths()
958
1034
  //===========
959
1035
  {
1036
+ console.log('Depracated API function called: togglePaths()')
960
1037
  if (this._disabledPathFeatures){
961
1038
  this.enablePathFeatures_(true, this._pathways.allFeatureIds());
962
1039
  this._disabledPathFeatures = false;
@@ -965,36 +1042,12 @@ export class UserInteractions
965
1042
  }
966
1043
  }
967
1044
 
968
- pathTypes()
969
- //=========
970
- {
971
- return this._pathways.pathTypes;
972
- }
973
-
974
1045
  enablePath(pathType, enable=true)
975
1046
  //===============================
976
1047
  {
977
1048
  this.enablePathFeatures_(enable, this._pathways.typeFeatureIds(pathType));
978
1049
  }
979
1050
 
980
- showPaths(pathTypes, enable=true)
981
- //===============================
982
- {
983
- // Disable/enable all paths except those with `pathTypes`
984
-
985
- this.enablePathFeatures_(!enable, this._pathways.allFeatureIds());
986
-
987
- if (Array.isArray(pathTypes)) {
988
- for (const pathType of pathTypes) {
989
- this.enablePath(pathType, enable);
990
- }
991
- } else {
992
- this.enablePath(pathTypes, enable);
993
- }
994
-
995
- this._disabledPathFeatures = true;
996
- }
997
-
998
1051
  pathwaysFeatureIds(externalIds)
999
1052
  //=============================
1000
1053
  {
@@ -1010,46 +1063,57 @@ export class UserInteractions
1010
1063
  return this._pathways.nodePathModels(nodeId);
1011
1064
  }
1012
1065
 
1013
- //==============================================================================
1014
-
1015
- // Find where to place a label or popup on a feature
1066
+ enableCentrelines(show=true)
1067
+ //==========================
1068
+ {
1069
+ this.enablePath('centreline', show);
1070
+ this._layerManager.setPaint({showCentrelines: show});
1071
+ }
1016
1072
 
1017
- __centralPosition(featureId, annotation)
1073
+ enableSckanPath(sckanState, enable=true)
1018
1074
  //======================================
1019
1075
  {
1020
- if (this.__centralPositions.has(featureId)) {
1021
- return this.__centralPositions.get(featureId);
1022
- }
1023
- let position = annotation.centroid;
1024
- const features = this._map.querySourceFeatures(VECTOR_TILES_SOURCE, {
1025
- 'sourceLayer': this._flatmap.options.separateLayers
1026
- ? `${annotation['layer']}_${annotation['tile-layer']}`
1027
- : annotation['tile-layer'],
1028
- 'filter': [
1029
- 'all',
1030
- [ '==', ['id'], parseInt(featureId) ],
1031
- [ '==', ['geometry-type'], 'Polygon' ]
1032
- ]
1033
- });
1034
- if (features.length > 0) {
1035
- const feature = features[0];
1036
- const polygon = feature.geometry.coordinates;
1037
- // Rough heuristic. Area is in km^2; below appears to be good enough.
1038
- const precision = ('area' in feature.properties)
1039
- ? Math.sqrt(feature.properties.area)/500000
1040
- : 0.1;
1041
- position = polylabel(polygon, precision);
1042
- }
1043
- this.__centralPositions.set(featureId, position);
1044
- return position;
1076
+ this._layerManager.enableSckanPath(sckanState, enable);
1045
1077
  }
1046
1078
 
1047
1079
  //==============================================================================
1048
1080
 
1049
1081
  // Marker handling
1050
1082
 
1051
- addMarker(anatomicalId, markerType='')
1052
- //====================================
1083
+ __markerPosition(featureId, annotation)
1084
+ {
1085
+ if (this.__markerPositions.has(featureId)) {
1086
+ return this.__markerPositions.get(featureId);
1087
+ }
1088
+ let position = annotation.markerPosition || annotation.centroid;
1089
+ if (position === null || position == undefined) {
1090
+ // Find where to place a label or popup on a feature
1091
+ const features = this._map.querySourceFeatures(VECTOR_TILES_SOURCE, {
1092
+ 'sourceLayer': this._flatmap.options.separateLayers
1093
+ ? `${annotation['layer']}_${annotation['tile-layer']}`
1094
+ : annotation['tile-layer'],
1095
+ 'filter': [
1096
+ 'all',
1097
+ [ '==', ['id'], parseInt(featureId) ],
1098
+ [ '==', ['geometry-type'], 'Polygon' ]
1099
+ ]
1100
+ });
1101
+ if (features.length > 0) {
1102
+ const feature = features[0];
1103
+ const polygon = feature.geometry.coordinates;
1104
+ // Rough heuristic. Area is in km^2; below appears to be good enough.
1105
+ const precision = ('area' in feature.properties)
1106
+ ? Math.sqrt(feature.properties.area)/500000
1107
+ : 0.1;
1108
+ position = polylabel(polygon, precision);
1109
+ }
1110
+ }
1111
+ this.__markerPositions.set(featureId, position);
1112
+ return position;
1113
+ }
1114
+
1115
+ addMarker(anatomicalId, options={})
1116
+ //=================================
1053
1117
  {
1054
1118
  const featureIds = this._flatmap.modelFeatureIds(anatomicalId);
1055
1119
  let markerId = -1;
@@ -1065,19 +1129,21 @@ export class UserInteractions
1065
1129
  markerId = this.__lastMarkerId;
1066
1130
  }
1067
1131
 
1132
+ // MapLibre dynamically sets a transform on marker elements so in
1133
+ // order to apply a scale transform we need to create marker icons
1134
+ // inside the marker container <div>.
1135
+ const colour = options.colour || '#005974';
1136
+ const markerHTML = options.element ? new maplibre.Marker({element: options.element})
1137
+ : new maplibre.Marker({color: colour});
1138
+
1068
1139
  const markerElement = document.createElement('div');
1069
1140
  const markerIcon = document.createElement('div');
1070
- if (markerType === 'simulation') {
1071
- markerIcon.innerHTML = this._simulationMarkerHTML;
1072
- } else {
1073
- markerIcon.innerHTML = this._defaultMarkerHTML;
1074
- }
1141
+ markerIcon.innerHTML = markerHTML.getElement().innerHTML;
1075
1142
  markerIcon.className = 'flatmap-marker';
1143
+ markerElement.id = `marker-${markerId}`;
1076
1144
  markerElement.appendChild(markerIcon);
1077
1145
 
1078
- const markerPosition = (annotation.geometry === 'Polygon')
1079
- ? this.__centralPosition(featureId, annotation)
1080
- : annotation.centroid;
1146
+ const markerPosition = this.__markerPosition(featureId, annotation);
1081
1147
  const marker = new maplibre.Marker(markerElement)
1082
1148
  .setLngLat(markerPosition)
1083
1149
  .addTo(this._map);
@@ -1091,6 +1157,7 @@ export class UserInteractions
1091
1157
  this.markerMouseEvent_.bind(this, marker, anatomicalId));
1092
1158
 
1093
1159
  this.__markerIdByMarker.set(marker, markerId);
1160
+ this.__markerIdByFeatureId.set(+featureId, markerId);
1094
1161
  this.__annotationByMarkerId.set(markerId, annotation);
1095
1162
  }
1096
1163
  }