@abi-software/flatmap-viewer 2.6.1 → 2.7.0-a.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
@@ -26,7 +26,7 @@ Running
26
26
 
27
27
  ::
28
28
 
29
- $ npm start
29
+ $ npm run dev
30
30
 
31
31
  Maps can then be viewed at http://localhost:3000
32
32
 
@@ -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.6.1``
41
+ * ``npm install @abi-software/flatmap-viewer@2.6.2``
42
42
 
43
43
  Documentation
44
44
  -------------
package/lib/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ //==============================================================================
2
+
3
+ import {FlatMap, MapManager} from '../src/flatmap-viewer'
4
+
5
+ //==============================================================================
6
+
7
+ export {FlatMap, MapManager}
8
+
9
+ //==============================================================================
package/package.json CHANGED
@@ -1,26 +1,28 @@
1
1
  {
2
2
  "name": "@abi-software/flatmap-viewer",
3
- "version": "2.6.1",
3
+ "version": "2.7.0-a.1",
4
4
  "description": "Flatmap viewer using Maplibre GL",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/AnatomicMaps/flatmap-viewer.git"
8
8
  },
9
+ "type": "module",
9
10
  "main": "src/main.js",
10
11
  "files": [
12
+ "lib",
11
13
  "src",
12
14
  "static"
13
15
  ],
14
16
  "scripts": {
15
17
  "test": "echo \"Error: no test specified\" && exit 1",
16
- "start": "node app",
17
- "build": "webpack --mode production",
18
+ "dev": "vite serve app --port 3000",
19
+ "build": "tsc --p ./tsconfig-build.json && vite build",
20
+ "preview": "vite preview",
18
21
  "docs": "cd docs; poetry run make html"
19
22
  },
20
23
  "author": "David Brooks",
21
24
  "license": "MIT",
22
25
  "dependencies": {
23
- "@babel/runtime": "^7.10.4",
24
26
  "@deck.gl/core": "^8.9.35",
25
27
  "@deck.gl/layers": "^8.9.35",
26
28
  "@deck.gl/mapbox": "^8.9.35",
@@ -33,28 +35,16 @@
33
35
  "colord": "^2.9.3",
34
36
  "core-js-pure": "^3.36.1",
35
37
  "html-es6cape": "^2.0.2",
36
- "maplibre-gl": ">=3.6.0",
38
+ "maplibre-gl": ">=4.1.0",
37
39
  "mathjax-full": "^3.2.2",
38
40
  "minisearch": "^2.2.1",
39
41
  "polylabel": "^1.1.0"
40
42
  },
41
43
  "devDependencies": {
42
- "@babel/core": "^7.5.5",
43
- "@babel/plugin-transform-runtime": "^7.5.5",
44
- "@babel/preset-env": "^7.10.4",
45
- "babel-loader": "^8.1.0",
46
- "browser-sync": "^2.26.7",
47
- "bs-fullscreen-message": "^1.1.0",
48
- "clean-webpack-plugin": "^3.0.0",
49
- "css-loader": "^6.7.3",
44
+ "@types/node": "^20.12.7",
50
45
  "eslint": "^8.7.0",
51
- "express": "^4.17.1",
52
- "html-webpack-plugin": "^4.5.2",
53
- "strip-ansi": "^7.0.1",
54
- "style-loader": "^3.3.2",
55
- "webpack": "^5.16.0",
56
- "webpack-cli": "^4.4.0",
57
- "webpack-dev-middleware": "^4.1.0",
58
- "webpack-node-externals": "^1.7.2"
46
+ "typescript": "^5.2.2",
47
+ "vite": "^5.1.4",
48
+ "vite-plugin-dts": "^3.8.1"
59
49
  }
60
50
  }
@@ -292,7 +292,7 @@ export class AnnotationDrawControl
292
292
  }
293
293
 
294
294
  changeMode(type)
295
- //===============
295
+ //==============
296
296
  {
297
297
  // Change the mode directly without listening to modes callback
298
298
  this.__draw.changeMode(type.mode, type.options)
@@ -18,7 +18,7 @@ limitations under the License.
18
18
 
19
19
  ******************************************************************************/
20
20
 
21
- export class Path3DControl
21
+ export class FlightPathControl
22
22
  {
23
23
  #button
24
24
  #container
@@ -26,9 +26,10 @@ export class Path3DControl
26
26
  #map = null
27
27
  #flatmap
28
28
 
29
- constructor(flatmap)
29
+ constructor(flatmap, enabled)
30
30
  {
31
31
  this.#flatmap = flatmap
32
+ this.#enabled = !!enabled
32
33
  }
33
34
 
34
35
  getDefaultPosition()
@@ -46,11 +47,15 @@ export class Path3DControl
46
47
  this.#button = document.createElement('button')
47
48
  this.#button.className = 'control-button text-button'
48
49
  this.#button.setAttribute('type', 'button')
49
- this.#button.setAttribute('aria-label', 'Show 3D paths')
50
+ this.#button.setAttribute('aria-label', 'Show flight paths')
50
51
  this.#button.textContent = '3D'
51
- this.#button.title = 'Show/hide 3D paths'
52
+ this.#button.title = 'Show/hide flight paths'
52
53
  this.#container.appendChild(this.#button)
53
54
  this.#container.addEventListener('click', this.onClick.bind(this))
55
+ if (this.#enabled) {
56
+ this.#button.classList.add('control-active')
57
+ this.__setBackground()
58
+ }
54
59
  return this.#container
55
60
  }
56
61
 
@@ -75,11 +80,11 @@ export class Path3DControl
75
80
  //=============
76
81
  {
77
82
  if (this.#button.classList.contains('control-active')) {
78
- this.#flatmap.enable3dPaths(false)
83
+ this.#flatmap.enableFlightPaths(false)
79
84
  this.#button.classList.remove('control-active')
80
85
  this.#enabled = false
81
86
  } else {
82
- this.#flatmap.enable3dPaths(true)
87
+ this.#flatmap.enableFlightPaths(true)
83
88
  this.#button.classList.add('control-active')
84
89
  this.#enabled = true
85
90
  }
@@ -33,12 +33,14 @@ import '../static/css/flatmap-viewer.css';
33
33
 
34
34
  //==============================================================================
35
35
 
36
- import {MapServer, loadJSON} from './mapserver.js';
36
+ import {MapServer} from './mapserver.js';
37
37
  import {SearchIndex} from './search.js';
38
38
  import {UserInteractions} from './interactions.js';
39
39
 
40
40
  import {APINATOMY_PATH_PREFIX} from './pathways';
41
41
 
42
+ import {loadClusterIcons} from './layers/cluster'
43
+
42
44
  import * as images from './images.js';
43
45
  import * as utils from './utils.js';
44
46
 
@@ -61,7 +63,7 @@ export const UNCLASSIFIED_TAXON_ID = 'NCBITaxon:2787823'; // unclassified entr
61
63
  * Maps are not created directly but instead are created and loaded by
62
64
  * :meth:`LoadMap` of :class:`MapManager`.
63
65
  */
64
- class FlatMap
66
+ export class FlatMap
65
67
  {
66
68
  constructor(container, mapBaseUrl, mapDescription, resolve)
67
69
  {
@@ -204,6 +206,9 @@ class FlatMap
204
206
  await this.addImage(image.id, image.url, '', image.options);
205
207
  }
206
208
 
209
+ // Load icons used for clustered markers
210
+ await loadClusterIcons(this._map)
211
+
207
212
  // Layers have now loaded so finish setting up
208
213
  this._userInteractions = new UserInteractions(this);
209
214
  }
@@ -901,15 +906,15 @@ class FlatMap
901
906
  }
902
907
 
903
908
  /**
904
- * Show/hide 3D path view.
909
+ * Show/hide flight path view.
905
910
  *
906
911
  * @param {boolean} [enable=true]
907
912
  */
908
- enable3dPaths(enable=true)
909
- //========================
913
+ enableFlightPaths(enable=true)
914
+ //============================
910
915
  {
911
916
  if (this._userInteractions !== null) {
912
- this._userInteractions.enable3dPaths(enable)
917
+ this._userInteractions.enableFlightPaths(enable)
913
918
  }
914
919
  }
915
920
 
@@ -966,6 +971,21 @@ class FlatMap
966
971
  return -1;
967
972
  }
968
973
 
974
+ addMarkers(anatomicalIds, options={})
975
+ //====================================
976
+ {
977
+ const markerIds = []
978
+ for (const anatomicalId of anatomicalIds) {
979
+ if (this._userInteractions !== null) {
980
+ markerIds.push(this._userInteractions.addMarker(anatomicalId, options))
981
+ } else {
982
+ markerIds.push(-1)
983
+ }
984
+ }
985
+ return markerIds
986
+ }
987
+
988
+
969
989
  /**
970
990
  * Remove a marker from the map.
971
991
  *
@@ -975,7 +995,7 @@ class FlatMap
975
995
  removeMarker(markerId)
976
996
  //====================
977
997
  {
978
- if (this._userInteractions !== null) {
998
+ if (markerId > -1 && this._userInteractions !== null) {
979
999
  this._userInteractions.removeMarker(markerId);
980
1000
  }
981
1001
  }
@@ -1046,19 +1066,17 @@ class FlatMap
1046
1066
  'alert',
1047
1067
  'biological-sex'
1048
1068
  ];
1049
- const jsonProperties = [
1050
- 'hyperlinks'
1051
- ];
1052
1069
  for (const property of exportedProperties) {
1053
1070
  if (property in properties) {
1054
1071
  const value = properties[property];
1055
1072
  if (value !== undefined) {
1056
- if (jsonProperties.includes(property)) {
1057
- data[property] = JSON.parse(properties[property])
1073
+ if ((Array.isArray(value) && value.length)
1074
+ || (value.constructor === Object && Object.keys(value).length)) {
1075
+ data[property] = value
1058
1076
  } else if (property === 'featureId') {
1059
- data[property] = +properties[property]; // Ensure numeric
1077
+ data[property] = +value; // Ensure numeric
1060
1078
  } else {
1061
- data[property] = properties[property];
1079
+ data[property] = value;
1062
1080
  }
1063
1081
  }
1064
1082
  }
@@ -1569,6 +1587,7 @@ export class MapManager
1569
1587
  * @arg options {Object} Configurable options for the map.
1570
1588
  * @arg options.background {string} Background colour of flatmap. Defaults to ``white``.
1571
1589
  * @arg options.debug {boolean} Enable debugging mode.
1590
+ * @arg options.flightPaths {boolean} Enable flight path (3D) view of neuron paths
1572
1591
  * @arg options.fullscreenControl {boolean} Add a ``Show full screen`` button to the map.
1573
1592
  * @arg options.layerOptions {Object} Options to control colour and outlines of features
1574
1593
  * @arg options.layerOptions.colour {boolean} Use colour fill (if available) for features. Defaults to ``true``.
@@ -43,7 +43,7 @@ import {AnnotatorControl, BackgroundControl, LayerControl, NerveControl,
43
43
  SCKANControl} from './controls/controls';
44
44
  import {AnnotationDrawControl, DRAW_ANNOTATION_LAYERS} from './controls/annotation'
45
45
  import {PathControl} from './controls/paths';
46
- import {Path3DControl} from './controls/paths3d'
46
+ import {FlightPathControl} from './controls/flightpaths'
47
47
  import {SearchControl} from './controls/search';
48
48
  import {MinimapControl} from './controls/minimap';
49
49
  import {SystemsControl} from './controls/systems';
@@ -180,12 +180,14 @@ export class UserInteractions
180
180
  const mapPathTypes = this.__pathManager.pathTypes();
181
181
 
182
182
  // Add and manage our layers. NB. this needs to after we have a
183
- // path manager but before path enabled state is set.
183
+ // path manager but before paths are enabled
184
184
 
185
185
  this._layerManager = new LayerManager(flatmap, this);
186
186
 
187
187
  // Set initial enabled state of paths
188
188
 
189
+ this.__pathManager.enablePathLines(true, true)
190
+
189
191
  for (const path of mapPathTypes) {
190
192
  this.__pathManager.enablePathsByType(path.type, path.enabled, true);
191
193
  }
@@ -251,7 +253,7 @@ export class UserInteractions
251
253
  this._map.addControl(new TaxonsControl(flatmap));
252
254
  }
253
255
 
254
- this._map.addControl(new Path3DControl(this));
256
+ this._map.addControl(new FlightPathControl(this, flatmap.options.flightPaths));
255
257
 
256
258
  if (flatmap.options.annotator) {
257
259
  this._map.addControl(new AnnotatorControl(flatmap));
@@ -265,6 +267,11 @@ export class UserInteractions
265
267
  this.#annotationDrawControl = new AnnotationDrawControl(flatmap, false)
266
268
  this._map.addControl(this.#annotationDrawControl)
267
269
 
270
+ // Set initial path viewing mode
271
+ if (flatmap.options.flightPaths === true) {
272
+ this._layerManager.setFlightPathMode(true)
273
+ }
274
+
268
275
  // Handle mouse events
269
276
 
270
277
  this._map.on('click', this.clickEvent_.bind(this));
@@ -456,10 +463,10 @@ export class UserInteractions
456
463
  this._layerManager.activate(layerId, enable);
457
464
  }
458
465
 
459
- enable3dPaths(enable=true)
460
- //========================
466
+ enableFlightPaths(enable=true)
467
+ //============================
461
468
  {
462
- this._layerManager.set3dMode(enable)
469
+ this._layerManager.setFlightPathMode(enable)
463
470
  }
464
471
 
465
472
  getSystems()
@@ -1305,6 +1312,7 @@ export class UserInteractions
1305
1312
  // Marker handling
1306
1313
 
1307
1314
  __markerPosition(featureId, annotation)
1315
+ //=====================================
1308
1316
  {
1309
1317
  if (this.__markerPositions.has(featureId)) {
1310
1318
  return this.__markerPositions.get(featureId);
@@ -1364,23 +1372,29 @@ export class UserInteractions
1364
1372
  markerOptions.className = options.className;
1365
1373
  }
1366
1374
  const markerPosition = this.__markerPosition(featureId, annotation);
1367
- const marker = new maplibregl.Marker(markerOptions)
1368
- .setLngLat(markerPosition)
1369
- .addTo(this._map);
1370
- markerElement.addEventListener('mouseenter',
1371
- this.markerMouseEvent_.bind(this, marker, anatomicalId));
1372
- markerElement.addEventListener('mousemove',
1373
- this.markerMouseEvent_.bind(this, marker, anatomicalId));
1374
- markerElement.addEventListener('mouseleave',
1375
- this.markerMouseEvent_.bind(this, marker, anatomicalId));
1376
- markerElement.addEventListener('click',
1377
- this.markerMouseEvent_.bind(this, marker, anatomicalId));
1378
-
1379
- this.__markerIdByMarker.set(marker, markerId);
1380
- this.__markerIdByFeatureId.set(+featureId, markerId);
1381
- this.__annotationByMarkerId.set(markerId, annotation);
1382
- if (!this.__featureEnabled(this.mapFeature(+featureId))) {
1383
- markerElement.style.visibility = 'hidden';
1375
+ if (options.cluster && this._layerManager) {
1376
+ this._layerManager.addMarker(markerId, markerPosition, annotation)
1377
+ } else {
1378
+ const marker = new maplibregl.Marker(markerOptions)
1379
+ .setLngLat(markerPosition)
1380
+ .addTo(this._map);
1381
+
1382
+
1383
+ markerElement.addEventListener('mouseenter',
1384
+ this.markerMouseEvent_.bind(this, marker, anatomicalId));
1385
+ markerElement.addEventListener('mousemove',
1386
+ this.markerMouseEvent_.bind(this, marker, anatomicalId));
1387
+ markerElement.addEventListener('mouseleave',
1388
+ this.markerMouseEvent_.bind(this, marker, anatomicalId));
1389
+ markerElement.addEventListener('click',
1390
+ this.markerMouseEvent_.bind(this, marker, anatomicalId));
1391
+
1392
+ this.__markerIdByMarker.set(marker, markerId);
1393
+ this.__markerIdByFeatureId.set(+featureId, markerId);
1394
+ this.__annotationByMarkerId.set(markerId, annotation);
1395
+ if (!this.__featureEnabled(this.mapFeature(+featureId))) {
1396
+ markerElement.style.visibility = 'hidden';
1397
+ }
1384
1398
  }
1385
1399
  }
1386
1400
  }
@@ -1429,28 +1443,46 @@ export class UserInteractions
1429
1443
  return anatomicalIds;
1430
1444
  }
1431
1445
 
1446
+ // Separate out MapLibre specific code and result of mouse event (tooltip,
1447
+ // client message, etc) so clustering code can also use this to process
1448
+ // events.
1449
+
1432
1450
  markerMouseEvent_(marker, anatomicalId, event)
1433
1451
  //============================================
1434
1452
  {
1435
1453
  // No tooltip when context menu is open
1436
1454
  if (this._modal
1437
1455
  || (this.__activeMarker !== null && event.type === 'mouseleave')) {
1438
- return;
1456
+ return
1439
1457
  }
1440
1458
 
1441
1459
  if (['mouseenter', 'mouseleave', 'click'].includes(event.type)) {
1442
- this.__activeMarker = marker;
1460
+ this.__activeMarker = marker
1443
1461
 
1444
- // Remove any existing tooltips
1445
- this.removeTooltip_();
1446
- marker.setPopup(null);
1462
+ // Remove any tooltip
1463
+ marker.setPopup(null)
1447
1464
 
1448
1465
  // Reset cursor
1449
1466
  marker.getElement().style.cursor = 'default';
1450
1467
 
1468
+
1469
+ const markerId = this.__markerIdByMarker.get(marker)
1470
+ const annotation = this.__annotationByMarkerId.get(markerId)
1471
+
1472
+ this.markerEvent_(event, markerId, marker.getLngLat(),
1473
+ anatomicalId, annotation)
1474
+ }
1475
+ }
1476
+
1477
+ markerEvent_(event, markerId, markerPosition, anatomicalId, annotation)
1478
+ //=====================================================================
1479
+ {
1480
+ if (['mouseenter', 'mouseleave', 'click'].includes(event.type)) {
1481
+
1482
+ // Remove any existing tooltips
1483
+ this.removeTooltip_();
1484
+
1451
1485
  if (['mouseenter', 'click'].includes(event.type)) {
1452
- const markerId = this.__markerIdByMarker.get(marker);
1453
- const annotation = this.__annotationByMarkerId.get(markerId);
1454
1486
  // The marker's feature
1455
1487
  const feature = this.mapFeature(annotation.featureId);
1456
1488
  if (feature !== undefined) {
@@ -1464,13 +1496,12 @@ export class UserInteractions
1464
1496
  }
1465
1497
  // Show tooltip
1466
1498
  const html = this.tooltipHtml_(annotation, true);
1467
- this.__showToolTip(html, marker.getLngLat());
1499
+ this.__showToolTip(html, markerPosition);
1468
1500
 
1469
1501
  // Send marker event message
1470
1502
  this._flatmap.markerEvent(event.type, markerId, anatomicalId);
1471
1503
  }
1472
1504
  }
1473
- event.stopPropagation();
1474
1505
  }
1475
1506
 
1476
1507
  __clearActiveMarker()
@@ -1519,7 +1550,7 @@ export class UserInteractions
1519
1550
  .setLngLat(location)
1520
1551
  .setDOMContent(element);
1521
1552
 
1522
- // Set the merker tooltip and show it
1553
+ // Set the marker tooltip and show it
1523
1554
  marker.setPopup(this._tooltip);
1524
1555
  marker.togglePopup();
1525
1556
 
@@ -0,0 +1,191 @@
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
+ import {SvgManager, SvgTemplateManager} from '../../thirdParty/maplibre-gl-svg/src'
22
+
23
+ //==============================================================================
24
+
25
+ const markerLargeCircle = `<svg xmlns="http://www.w3.org/2000/svg" width="calc(28 * {scale})" height="calc(39 * {scale})" viewBox="-1 -1 27 42">
26
+ <ellipse style="fill: rgb(0, 0, 0); fill-opacity: 0.2;" cx="12" cy="36" rx="8" ry="4"/>
27
+ <path d="M12.25.25a12.254 12.254 0 0 0-12 12.494c0 6.444 6.488 12.109 11.059 22.564.549 1.256 1.333 1.256 1.882 0
28
+ C17.762 24.853 24.25 19.186 24.25 12.744A12.254 12.254 0 0 0 12.25.25Z"
29
+ style="fill:{color};stroke:{secondaryColor};stroke-width:1"/>
30
+ <circle cx="12.5" cy="12.5" r="9" fill="{secondaryColor}"/>
31
+ <text x="12" y="17.5" style="font-size:14px;fill:#000;text-anchor:middle">{text}</text>
32
+ </svg>`
33
+
34
+ const markerSmallCircle = `<svg xmlns="http://www.w3.org/2000/svg" width="calc(28 * {scale})" height="calc(39 * {scale})" viewBox="-1 -1 27 42">
35
+ <ellipse style="fill: rgb(0, 0, 0); fill-opacity: 0.2;" cx="12" cy="36" rx="8" ry="4"/>
36
+ <path d="M12.25.25a12.254 12.254 0 0 0-12 12.494c0 6.444 6.488 12.109 11.059 22.564.549 1.256 1.333 1.256 1.882 0
37
+ C17.762 24.853 24.25 19.186 24.25 12.744A12.254 12.254 0 0 0 12.25.25Z"
38
+ style="fill:{color};stroke:{secondaryColor};stroke-width:1"/>
39
+ <circle cx="12.5" cy="12.5" r="5" fill="{secondaryColor}"/>
40
+ </svg>`
41
+
42
+ //==============================================================================
43
+
44
+ export async function loadClusterIcons(map)
45
+ {
46
+ SvgTemplateManager.addTemplate('marker-large-circle', markerLargeCircle, false)
47
+ SvgTemplateManager.addTemplate('marker-small-circle', markerSmallCircle, false)
48
+
49
+ const svgManager = new SvgManager(map)
50
+ await svgManager.createFromTemplate('clustered-marker', 'marker-large-circle', '#EE5900', '#fff')
51
+ await svgManager.createFromTemplate('unclustered-marker', 'marker-small-circle', '#005974', '#fff')
52
+ }
53
+
54
+ //==============================================================================
55
+
56
+ export class ClusteredMarkerLayer
57
+ {
58
+ #flatmap
59
+ #map
60
+ #points = {
61
+ type: 'FeatureCollection',
62
+ features: []
63
+ }
64
+ #ui
65
+
66
+ seenmove = false
67
+
68
+ constructor(flatmap, ui)
69
+ {
70
+ this.#flatmap = flatmap
71
+ this.#ui = ui
72
+ this.#map = flatmap.map
73
+
74
+ this.#map.addSource('markers', {
75
+ type: 'geojson',
76
+ data: this.#points,
77
+ cluster: true, // Adds the ``point_count`` property to source data
78
+ clusterMaxZoom: 9, // Max zoom to cluster points on
79
+ clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
80
+ })
81
+
82
+ this.#map.addLayer({
83
+ id: 'clustered-markers',
84
+ type: 'symbol',
85
+ source: 'markers',
86
+ filter: ['has', 'point_count'],
87
+ 'layout': {
88
+ 'icon-image': 'clustered-marker',
89
+ 'icon-allow-overlap': true,
90
+ 'icon-ignore-placement': true,
91
+ 'icon-offset': [0, -17],
92
+ 'icon-size': 0.8,
93
+ 'text-field': '{point_count_abbreviated}',
94
+ 'text-size': 10,
95
+ 'text-offset': [0, -1.93]
96
+ }
97
+ })
98
+
99
+ this.#map.addLayer({
100
+ id: 'single-points',
101
+ type: 'symbol',
102
+ source: 'markers',
103
+ filter: ['!', ['has', 'point_count']],
104
+ 'layout': {
105
+ 'icon-image': 'unclustered-marker',
106
+ 'icon-allow-overlap': true,
107
+ 'icon-ignore-placement': true,
108
+ 'icon-offset': [0, -17],
109
+ 'icon-size': 0.6
110
+ }
111
+ })
112
+
113
+ // inspect a cluster on click
114
+ this.#map.on('click', 'clustered-markers', async (e) => {
115
+ const features = this.#map.queryRenderedFeatures(e.point, {
116
+ layers: ['clustered-markers']
117
+ })
118
+ console.log('Cluster marker', features)
119
+ const clusterId = features[0].properties.cluster_id
120
+ const zoom = await this.#map.getSource('markers').getClusterExpansionZoom(clusterId)
121
+ this.#map.easeTo({
122
+ center: features[0].geometry.coordinates,
123
+ zoom
124
+ })
125
+ })
126
+
127
+ this.#map.on('click', 'single-points', this.singleMarkerEvent.bind(this))
128
+ this.#map.on('mouseenter', 'single-points', this.singleMarkerEvent.bind(this))
129
+ this.#map.on('mousemove', 'single-points', this.singleMarkerEvent.bind(this))
130
+ // this.#map.on('mouseleave', 'single-points', this.singleMarkerEvent.bind(this))
131
+
132
+ this.#map.on('mouseenter', 'clustered-markers', () => {
133
+ this.#map.getCanvas().style.cursor = 'pointer'
134
+ })
135
+
136
+ this.#map.on('mouseleave', 'clustered-markers', () => {
137
+ this.#map.getCanvas().style.cursor = ''
138
+ })
139
+
140
+ // Also 'mousemove'...
141
+ }
142
+
143
+ singleMarkerEvent(event)
144
+ //======================
145
+ {
146
+ const features = this.#map.queryRenderedFeatures(event.point, {
147
+ layers: ['single-points']
148
+ })
149
+ for (const feature of features) {
150
+ if (event.type === 'mousemove' && !this.seenMove) {
151
+ console.log('Single marker', event.type, feature)
152
+ this.seenMove = true
153
+ } else if (event.type !== 'mousemove') {
154
+ console.log('Single marker', event.type, feature)
155
+ this.seenMove = false
156
+ }
157
+
158
+ const properties = feature.properties
159
+ const position = properties.markerPosition.slice(1, -1).split(',').map(p => +p)
160
+ this.#ui.markerEvent_(event, feature.id, position, properties.models, properties)
161
+ }
162
+ event.originalEvent.stopPropagation()
163
+ }
164
+
165
+ addMarker(id, position, properties={})
166
+ //====================================
167
+ {
168
+ this.#points.features.push({
169
+ type: 'Feature',
170
+ id,
171
+ properties,
172
+ geometry: {
173
+ type: 'Point',
174
+ coordinates: position
175
+ }
176
+ })
177
+ this.#map.getSource('markers')
178
+ .setData(this.#points)
179
+ }
180
+
181
+ clearMarkers()
182
+ //============
183
+ {
184
+ this.#points.features = []
185
+ this.#map.getSource('markers')
186
+ .setData(this.#points)
187
+ }
188
+ }
189
+
190
+ //==============================================================================
191
+
@@ -135,7 +135,7 @@ class ArcDashedLayer extends ArcMapLayer
135
135
 
136
136
  //==============================================================================
137
137
 
138
- export class Paths3DLayer
138
+ export class FlightPathLayer
139
139
  {
140
140
  #arcLayers = new Map()
141
141
  #deckOverlay = null
@@ -344,14 +344,12 @@ export class Paths3DLayer
344
344
  id: `arc-${pathType}`,
345
345
  data: pathData,
346
346
  pickable: true,
347
- autoHighlight: true,
348
347
  numSegments: 400,
349
348
  // Styles
350
349
  getSourcePosition: f => f.pathStartPosition,
351
350
  getTargetPosition: f => f.pathEndPosition,
352
351
  getSourceColor: this.#pathColour.bind(this),
353
352
  getTargetColor: this.#pathColour.bind(this),
354
- highlightColor: o => this.#pathColour(o.object),
355
353
  opacity: 1.0,
356
354
  getWidth: 3,
357
355
  }
@@ -366,13 +364,14 @@ export class Paths3DLayer
366
364
  source: 'vector-tiles',
367
365
  sourceLayer: `${pickedObject.layer}_${pickedObject['tile-layer']}`,
368
366
  properties: pickedObject,
369
- arc3dLayer: true
367
+ flightPath: true
370
368
  }
371
369
  }
372
370
 
373
371
  #setupDeckOverlay()
374
372
  //=================
375
373
  {
374
+ // One overlay layer for each path style
376
375
  [...this.#pathStyles.values()].filter(style => this.#pathManager.pathTypeEnabled(style.type))
377
376
  .forEach(style => this.#addArcLayer(style.type))
378
377
  this.#deckOverlay = new DeckOverlay({
@@ -25,9 +25,10 @@ limitations under the License.
25
25
  import {PATHWAYS_LAYER} from '../pathways.js';
26
26
  import * as utils from '../utils.js';
27
27
 
28
+ import {ClusteredMarkerLayer} from './cluster'
28
29
  import * as style from './styling.js';
29
30
 
30
- import {Paths3DLayer} from './paths3d'
31
+ import {FlightPathLayer} from './flightpaths'
31
32
  import {PropertiesFilter} from './filter'
32
33
 
33
34
  const FEATURES_LAYER = 'features';
@@ -183,8 +184,8 @@ class MapFeatureLayers extends MapStylingLayers
183
184
  }
184
185
  }
185
186
 
186
- enablePaths2dLayer(visible)
187
- //=========================
187
+ setFlatPathMode(visible)
188
+ //======================
188
189
  {
189
190
  for (const layer of this.#pathLayers) {
190
191
  this.map.setLayoutProperty(layer.id, 'visibility', visible ? 'visible' : 'none')
@@ -266,7 +267,8 @@ class MapRasterLayers extends MapStylingLayers
266
267
  export class LayerManager
267
268
  {
268
269
  #featureLayers = new Map()
269
- #paths3dLayer = null
270
+ #markerLayer = null
271
+ #flightPathLayer = null
270
272
  #rasterLayer = null
271
273
 
272
274
  constructor(flatmap, ui)
@@ -306,8 +308,11 @@ export class LayerManager
306
308
  this.__layerOptions));
307
309
  }
308
310
 
309
- // Support 3D path view
310
- this.#paths3dLayer = new Paths3DLayer(flatmap, ui)
311
+ // Support flight path view
312
+ this.#flightPathLayer = new FlightPathLayer(flatmap, ui)
313
+
314
+ // Show clustered markers in a layer
315
+ this.#markerLayer = new ClusteredMarkerLayer(flatmap, ui)
311
316
  }
312
317
 
313
318
  get layers()
@@ -356,12 +361,24 @@ export class LayerManager
356
361
  }
357
362
  }
358
363
 
364
+ addMarker(id, position, properties={})
365
+ //====================================
366
+ {
367
+ this.#markerLayer.addMarker(id, position, properties)
368
+ }
369
+
370
+ clearMarkers()
371
+ //============
372
+ {
373
+ this.#markerLayer.clearMarkers()
374
+ }
375
+
359
376
  featuresAtPoint(point)
360
377
  //====================
361
378
  {
362
379
  let features = []
363
- if (this.#paths3dLayer) {
364
- features = this.#paths3dLayer.queryFeaturesAtPoint(point)
380
+ if (this.#flightPathLayer) {
381
+ features = this.#flightPathLayer.queryFeaturesAtPoint(point)
365
382
  }
366
383
  if (features.length === 0) {
367
384
  features = this.__map.queryRenderedFeatures(point)
@@ -372,16 +389,16 @@ export class LayerManager
372
389
  removeFeatureState(feature, key)
373
390
  //==============================
374
391
  {
375
- if (this.#paths3dLayer) {
376
- this.#paths3dLayer.removeFeatureState(feature.id, key)
392
+ if (this.#flightPathLayer) {
393
+ this.#flightPathLayer.removeFeatureState(feature.id, key)
377
394
  }
378
395
  }
379
396
 
380
397
  setFeatureState(feature, state)
381
398
  //=============================
382
399
  {
383
- if (this.#paths3dLayer) {
384
- this.#paths3dLayer.setFeatureState(feature.id, state)
400
+ if (this.#flightPathLayer) {
401
+ this.#flightPathLayer.setFeatureState(feature.id, state)
385
402
  }
386
403
  }
387
404
 
@@ -395,8 +412,8 @@ export class LayerManager
395
412
  for (const mapLayer of this.#featureLayers.values()) {
396
413
  mapLayer.setPaint(this.__layerOptions)
397
414
  }
398
- if (this.#paths3dLayer) {
399
- this.#paths3dLayer.setPaint(options)
415
+ if (this.#flightPathLayer) {
416
+ this.#flightPathLayer.setPaint(options)
400
417
  }
401
418
  }
402
419
 
@@ -407,27 +424,31 @@ export class LayerManager
407
424
  for (const mapLayer of this.#featureLayers.values()) {
408
425
  mapLayer.setFilter(this.__layerOptions);
409
426
  }
410
- if (this.#paths3dLayer) {
427
+ if (this.#flightPathLayer) {
428
+ // * @arg options.layerOptions.sckan {string} Show neuron paths known to SCKAN: values are ``valid`` (default),
429
+ // * ``invalid``, ``all`` or ``none``.
430
+
431
+
411
432
  const sckanState = options.sckan || 'valid'
412
433
  const sckanFilter = (sckanState == 'none') ? {NOT: {HAS: 'sckan'}} :
413
434
  (sckanState == 'valid') ? {sckan: true} :
414
- (sckanState == 'invalid') ? {NOT: {sckan: true}} :
435
+ (sckanState == 'invalid') ? {NOT: {sckan: true}} : // {sckan: false} is different...
415
436
  true
416
437
  const featureFilter = new PropertiesFilter(sckanFilter)
417
438
  if ('taxons' in options) {
418
439
  featureFilter.narrow({taxons: options.taxons})
419
440
  }
420
- this.#paths3dLayer.setFilter(featureFilter)
441
+ this.#flightPathLayer.setFilter(featureFilter)
421
442
  }
422
443
  }
423
444
 
424
- set3dMode(enable=true)
425
- //====================
445
+ setFlightPathMode(enable=true)
446
+ //============================
426
447
  {
427
- if (this.#paths3dLayer) {
428
- this.#paths3dLayer.enable(enable)
448
+ if (this.#flightPathLayer) {
449
+ this.#flightPathLayer.enable(enable)
429
450
  for (const mapLayer of this.#featureLayers.values()) {
430
- mapLayer.enablePaths2dLayer(!enable)
451
+ mapLayer.setFlatPathMode(!enable)
431
452
  }
432
453
  }
433
454
  }
package/src/main.js CHANGED
@@ -25,6 +25,41 @@ limitations under the License.
25
25
  import { MapManager } from './flatmap-viewer';
26
26
  export { MapManager };
27
27
 
28
+ const ALL_MARKERS = [
29
+ // Most of these are around the heart
30
+ 'UBERON:0003382',
31
+ 'UBERON:0002348',
32
+ 'UBERON:0002349',
33
+ 'UBERON:0003379',
34
+ 'UBERON:0001986',
35
+ 'UBERON:0001074',
36
+ 'UBERON:0003381',
37
+ 'UBERON:0002165',
38
+ 'UBERON:0002080',
39
+ 'UBERON:0000948',
40
+ 'UBERON:0002084',
41
+ 'UBERON:0002078',
42
+ 'UBERON:0002079',
43
+ 'UBERON:0002349',
44
+ 'UBERON:0002408',
45
+ 'UBERON:0007240',
46
+ 'UBERON:0002359',
47
+
48
+ 'UBERON:0001508',
49
+ 'UBERON:0037094',
50
+ 'ILX:0738305',
51
+
52
+ 'UBERON:0000948', // {className: 'heart-marker'}); // Heart
53
+ 'UBERON:0002048', // Lung
54
+ 'UBERON:0000945', // Stomach
55
+ 'UBERON:0001155', // Colon
56
+ 'UBERON:0001255', // Bladder
57
+ 'UBERON:0001759', // Vagus
58
+
59
+ 'UBERON:0016508', // Pelvic ganglion
60
+
61
+ ]
62
+
28
63
  //==============================================================================
29
64
 
30
65
  class DrawControl
@@ -110,6 +145,7 @@ export async function standaloneViewer(map_endpoint=null, options={})
110
145
  showPosition: false,
111
146
  standalone: true,
112
147
  annotator: true,
148
+ flightPaths: true
113
149
  }, options);
114
150
 
115
151
  function loadMap(id, taxon, sex)
@@ -139,15 +175,20 @@ export async function standaloneViewer(map_endpoint=null, options={})
139
175
  mapOptions.background = args[0].value;
140
176
  } else if (eventType === 'annotation') {
141
177
  drawControl.handleEvent(...args)
178
+ } else {
179
+ //console.log(eventType, ...args)
142
180
  }
143
181
  }, mapOptions)
144
182
  .then(map => {
183
+ /*
145
184
  map.addMarker('UBERON:0000948', {className: 'heart-marker'}); // Heart
146
185
  map.addMarker('UBERON:0002048'); // Lung
147
186
  map.addMarker('UBERON:0000945'); // Stomach
148
187
  map.addMarker('UBERON:0001155'); // Colon
149
188
  map.addMarker('UBERON:0001255'); // Bladder
150
189
  map.addMarker('UBERON:0001759'); // Vagus
190
+ */
191
+ map.addMarkers(ALL_MARKERS, {cluster: true})
151
192
  currentMap = map;
152
193
  drawControl = new DrawControl(map)
153
194
  })
package/src/pathways.js CHANGED
@@ -61,7 +61,7 @@ export function pathColourArray(pathType, alpha=255)
61
61
  {
62
62
  const rgb = colord(PathTypeMap.has(pathType)
63
63
  ? PathTypeMap.get(pathType).colour
64
- : '#FF0').toRgb()
64
+ : PathTypeMap.get('other').colour).toRgb()
65
65
  return [rgb.r, rgb.g, rgb.b, alpha]
66
66
  }
67
67
 
@@ -92,9 +92,6 @@ export class PathManager
92
92
  pathLines[pathId] = path.lines;
93
93
  pathNerves[pathId] = path.nerves;
94
94
  this.__paths[pathId] = path;
95
- for (const lineId of path.lines) {
96
- this.__ui.enableFeature(lineId, enabled, true);
97
- }
98
95
  this.__paths[pathId].systemCount = 0;
99
96
  if ('models' in path) {
100
97
  const modelId = path['models'];
@@ -316,6 +313,14 @@ export class PathManager
316
313
  return featureIds;
317
314
  }
318
315
 
316
+ enablePathLines(enable, force=false)
317
+ //==================================
318
+ {
319
+ for (const lineId of Object.keys(this.__pathsByLine)) {
320
+ this.__ui.enableFeature(lineId, enable, force)
321
+ }
322
+ }
323
+
319
324
  enablePathsBySystem(system, enable, force=false)
320
325
  //==============================================
321
326
  {
package/src/types.ts ADDED
@@ -0,0 +1,26 @@
1
+ /******************************************************************************
2
+
3
+ Flatmap viewer and annotation tool
4
+
5
+ Copyright (c) 2019 - 2024 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
+ export type Constructor<T> = new(...args: any[]) => T
22
+
23
+ export type ObjectRecord = Record<string, any>
24
+
25
+ //==============================================================================
26
+
package/src/utils.js CHANGED
@@ -120,6 +120,7 @@ export function normaliseId(id)
120
120
 
121
121
  export function setDefaults(options, defaultOptions)
122
122
  {
123
+ // c.f. Object.assign({}, defaultOptions, options)
123
124
  if (options === undefined || options === null) {
124
125
  return defaultOptions;
125
126
  }