@abi-software/flatmap-viewer 2.5.10 → 2.6.0

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.5.10``
41
+ * ``npm install @abi-software/flatmap-viewer@2.6.0``
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.5.10",
3
+ "version": "2.6.0",
4
4
  "description": "Flatmap viewer using Maplibre GL",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,9 +21,9 @@
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "@babel/runtime": "^7.10.4",
24
- "@deck.gl/core": "^8.9.33",
25
- "@deck.gl/layers": "^8.9.33",
26
- "@deck.gl/mapbox": "^8.9.33",
24
+ "@deck.gl/core": "^8.9.35",
25
+ "@deck.gl/layers": "^8.9.35",
26
+ "@deck.gl/mapbox": "^8.9.35",
27
27
  "@mapbox/mapbox-gl-draw": "^1.4.3",
28
28
  "@turf/area": "^6.5.0",
29
29
  "@turf/bbox": "^6.5.0",
@@ -31,6 +31,7 @@
31
31
  "@turf/projection": "^6.5.0",
32
32
  "bezier-js": "^6.1.0",
33
33
  "colord": "^2.9.3",
34
+ "core-js-pure": "^3.36.1",
34
35
  "html-es6cape": "^2.0.2",
35
36
  "maplibre-gl": ">=3.6.0",
36
37
  "mathjax-full": "^3.2.2",
@@ -264,14 +264,14 @@ export class AnnotationDrawControl
264
264
  }
265
265
  }
266
266
 
267
- clearFeature()
268
- //============
267
+ clearFeatures()
268
+ //=============
269
269
  {
270
270
  this.__draw.deleteAll()
271
271
  }
272
272
 
273
- trashFeature()
274
- //============
273
+ removeFeature()
274
+ //=============
275
275
  {
276
276
  this.__draw.trash()
277
277
  }
@@ -436,6 +436,21 @@ class FlatMap
436
436
  return this.__uuid;
437
437
  }
438
438
 
439
+ /**
440
+ * The map's URL on the map server.
441
+ *
442
+ * @type string
443
+ */
444
+ get url()
445
+ //========
446
+ {
447
+ let url = this.makeServerUrl('')
448
+ if (url.endsWith('/')) {
449
+ return url.substring(0, url.length - 1)
450
+ }
451
+ return url
452
+ }
453
+
439
454
  /**
440
455
  * The map's ``index.json`` as returned from the map server.
441
456
  *
@@ -1122,18 +1137,18 @@ class FlatMap
1122
1137
  //======================
1123
1138
  {
1124
1139
  if (this._userInteractions) {
1125
- this._userInteractions.clearAnnotationFeature()
1140
+ this._userInteractions.clearAnnotationFeatures()
1126
1141
  }
1127
1142
  }
1128
1143
 
1129
1144
  /**
1130
- * Fire trash to enter `updated` or `deleted` feature event.
1145
+ * Delete the selected drawn feature
1131
1146
  */
1132
- trashAnnotationFeature()
1133
- //======================
1147
+ removeAnnotationFeature()
1148
+ //=======================
1134
1149
  {
1135
1150
  if (this._userInteractions) {
1136
- this._userInteractions.trashAnnotationFeature()
1151
+ this._userInteractions.removeAnnotationFeature()
1137
1152
  }
1138
1153
  }
1139
1154
 
@@ -36,7 +36,6 @@ import polylabel from 'polylabel';
36
36
  import {LayerManager} from './layers';
37
37
  import {PATHWAYS_LAYER, PathManager} from './pathways';
38
38
  import {VECTOR_TILES_SOURCE} from './layers/styling';
39
- import {Paths3DLayer} from './layers/paths3d'
40
39
  import {SystemsManager} from './systems';
41
40
 
42
41
  import {displayedProperties, InfoControl} from './controls/info';
@@ -132,7 +131,6 @@ export class UserInteractions
132
131
  {
133
132
  #annotationDrawControl = null
134
133
  #minimap = null
135
- #paths3dLayer = null
136
134
 
137
135
  constructor(flatmap)
138
136
  {
@@ -166,9 +164,7 @@ export class UserInteractions
166
164
 
167
165
  flatmap.setInitialPosition();
168
166
 
169
- // Add and manage our layers
170
-
171
- this._layerManager = new LayerManager(flatmap);
167
+ // Track enabled features
172
168
 
173
169
  this.__featureEnabledCount = new Map(Array.from(this._flatmap.annotations.keys()).map(k => [+k, 0]));
174
170
 
@@ -180,9 +176,16 @@ export class UserInteractions
180
176
  this.__pathManager = new PathManager(flatmap, this, featuresEnabled);
181
177
 
182
178
  // The path types in this map
179
+
183
180
  const mapPathTypes = this.__pathManager.pathTypes();
184
181
 
182
+ // Add and manage our layers. NB. this needs to after we have a
183
+ // path manager but before path enabled state is set.
184
+
185
+ this._layerManager = new LayerManager(flatmap, this);
186
+
185
187
  // Set initial enabled state of paths
188
+
186
189
  for (const path of mapPathTypes) {
187
190
  this.__pathManager.enablePathsByType(path.type, path.enabled, true);
188
191
  }
@@ -216,9 +219,6 @@ export class UserInteractions
216
219
  this._map.addControl(new NavigationControl(flatmap), position);
217
220
  }
218
221
 
219
- // Support 3D path view
220
- this.#paths3dLayer = new Paths3DLayer(flatmap, this)
221
-
222
222
  // Add various controls when running standalone
223
223
  if (flatmap.options.standalone) {
224
224
  // Add a control to search annotations if option set
@@ -300,7 +300,8 @@ export class UserInteractions
300
300
  return {
301
301
  center: this._map.getCenter().toArray(),
302
302
  zoom: this._map.getZoom(),
303
- layers: this.layers
303
+ bearing: this._map.getBearing(),
304
+ pitch: this._map.getPitch()
304
305
  };
305
306
  }
306
307
 
@@ -308,14 +309,11 @@ export class UserInteractions
308
309
  //=============
309
310
  {
310
311
  // Restore the map to a saved state
311
- const options = {};
312
- if ('center' in state) {
313
- options['center'] = state.center;
314
- }
315
- if ('zoom' in state) {
316
- options['zoom'] = state.zoom;
317
- if ('center' in state) {
318
- options['around'] = state.center;
312
+
313
+ const options = Object.assign({}, state)
314
+ if ('zoom' in options) {
315
+ if ('center' in options) {
316
+ options['around'] = options.center;
319
317
  } else {
320
318
  options['around'] = [0, 0];
321
319
  }
@@ -365,19 +363,19 @@ export class UserInteractions
365
363
  }
366
364
  }
367
365
 
368
- clearAnnotationFeature()
369
- //======================
366
+ clearAnnotationFeatures()
367
+ //=======================
370
368
  {
371
369
  if (this.#annotationDrawControl) {
372
- this.#annotationDrawControl.clearFeature()
370
+ this.#annotationDrawControl.clearFeatures()
373
371
  }
374
372
  }
375
373
 
376
- trashAnnotationFeature()
377
- //======================
374
+ removeAnnotationFeature()
375
+ //=======================
378
376
  {
379
377
  if (this.#annotationDrawControl) {
380
- this.#annotationDrawControl.trashFeature()
378
+ this.#annotationDrawControl.removeFeature()
381
379
  }
382
380
  }
383
381
 
@@ -437,9 +435,6 @@ export class UserInteractions
437
435
  //================
438
436
  {
439
437
  this._layerManager.setPaint(options)
440
- if (this.#paths3dLayer) {
441
- this.#paths3dLayer.setPaint(options)
442
- }
443
438
  }
444
439
 
445
440
  setPaint(options)
@@ -464,9 +459,7 @@ export class UserInteractions
464
459
  enable3dPaths(enable=true)
465
460
  //========================
466
461
  {
467
- if (this.#paths3dLayer) {
468
- this.#paths3dLayer.enable(enable)
469
- }
462
+ this._layerManager.set3dMode(enable)
470
463
  }
471
464
 
472
465
  getSystems()
@@ -508,18 +501,14 @@ export class UserInteractions
508
501
  //===============================
509
502
  {
510
503
  this._map.removeFeatureState(feature, key)
511
- if (this.#paths3dLayer) {
512
- this.#paths3dLayer.removeFeatureState(feature.id, key)
513
- }
504
+ this._layerManager.removeFeatureState(feature, key)
514
505
  }
515
506
 
516
507
  #setFeatureState(feature, state)
517
508
  //==============================
518
509
  {
519
510
  this._map.setFeatureState(feature, state)
520
- if (this.#paths3dLayer) {
521
- this.#paths3dLayer.setFeatureState(feature.id, state)
522
- }
511
+ this._layerManager.setFeatureState(feature, state)
523
512
  }
524
513
 
525
514
  enableMapFeature(feature, enable=true)
@@ -992,13 +981,7 @@ export class UserInteractions
992
981
  #renderedFeatures(point)
993
982
  //======================
994
983
  {
995
- let features = []
996
- if (this.#paths3dLayer) {
997
- features = this.#paths3dLayer.queryFeaturesAtPoint(point)
998
- }
999
- if (features.length === 0) {
1000
- features = this._map.queryRenderedFeatures(point)
1001
- }
984
+ const features = this._layerManager.featuresAtPoint(point)
1002
985
  return features.filter(feature => this.__featureEnabled(feature));
1003
986
  }
1004
987
 
@@ -0,0 +1,310 @@
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
+ import Set from 'core-js-pure/actual/set'
22
+
23
+ //==============================================================================
24
+
25
+ export class PropertiesFilter
26
+ {
27
+ #filter
28
+
29
+ constructor(filter=true)
30
+ //======================
31
+ {
32
+ if (filter.constructor !== Object) { // We allow boolean values
33
+ this.#filter = filter
34
+ } else {
35
+ this.#filter = Object.assign({}, filter)
36
+ }
37
+ }
38
+
39
+ clear()
40
+ //=====
41
+ {
42
+ if (this.#filter !== true) {
43
+ this.#filter = true
44
+ }
45
+ }
46
+
47
+ expand(filter)
48
+ //============
49
+ {
50
+ if (this.#filter === false) {
51
+ this.#filter = filter
52
+ } else if (this.#filter !== true) {
53
+ const copiedFilter = Object.assign({}, filter)
54
+ this.#filter = { "OR": [this.#filter, copiedFilter] }
55
+ }
56
+ }
57
+
58
+ invert()
59
+ //======
60
+ {
61
+ if (this.#filter === false) {
62
+ this.#filter = true
63
+ } else if (this.#filter === true) {
64
+ this.#filter = false
65
+ } else {
66
+ const copiedFilter = Object.assign({}, filter)
67
+ this.#filter = { "NOT": copiedFilter }
68
+ }
69
+ }
70
+
71
+ getFilter()
72
+ //=========
73
+ {
74
+ return this.#filter
75
+ }
76
+
77
+ makeStyleFilter()
78
+ //===============
79
+ {
80
+ return this.#makeStyleFilter(this.#filter)
81
+ }
82
+
83
+ match(properties)
84
+ //===============
85
+ {
86
+ return this.#match(properties, this.#filter)
87
+ }
88
+
89
+ narrow(filter)
90
+ //============
91
+ {
92
+ if (this.#filter === true) {
93
+ this.#filter = filter
94
+ } else if (this.#filter !== false) {
95
+ const copiedFilter = Object.assign({}, filter)
96
+ this.#filter = { "AND": [this.#filter, copiedFilter] }
97
+ }
98
+ }
99
+
100
+ setFilter(filter)
101
+ //===============
102
+ {
103
+ if (filter.constructor !== Object) {
104
+ this.#filter = filter
105
+ } else {
106
+ this.#filter = Object.assign({}, filter)
107
+ }
108
+ }
109
+
110
+ #makeStyleFilter(filter)
111
+ //======================
112
+ {
113
+ if (filter.constructor !== Object) {
114
+ return !!filter
115
+ }
116
+ const styleFilter = []
117
+ for (const [key, expr] of Object.entries(filter)) {
118
+ if (key === 'AND' || key === 'OR') {
119
+ if (Array.isArray(expr) && expr.length >= 2) {
120
+ styleFilter.push((key === 'AND') ? 'all' : 'any',
121
+ ...expr.map(e => this.#makeStyleFilter(e)))
122
+ } else {
123
+ console.warn(`makeFilter: Invalid ${key} operands: ${expr}`)
124
+ }
125
+ } else if (key === 'HAS') {
126
+ styleFilter.push('has', expr)
127
+ } else if (key === 'NOT') {
128
+ const filterExpr = this.#makeStyleFilter(expr)
129
+ if (Array.isArray(filterExpr)) {
130
+ if (filterExpr.length === 2 && ['has', '!has'].includes(filterExpr[0])) {
131
+ if (filterExpr[0] === 'has') {
132
+ styleFilter.push('!has', filterExpr[1])
133
+ } else {
134
+ styleFilter.push('has', filterExpr[1])
135
+ }
136
+ } else if (filterExpr.length === 3 && ['==', '!='].includes(filterExpr[0])) {
137
+ if (filterExpr[0] === '==') {
138
+ styleFilter.push('!=', filterExpr[1], filterExpr[2])
139
+ } else {
140
+ styleFilter.push('==', filterExpr[1], filterExpr[2])
141
+ }
142
+ } else {
143
+ styleFilter.push('!', filterExpr)
144
+ }
145
+ } else {
146
+ styleFilter.push(!filterExpr)
147
+ }
148
+ } else {
149
+ if (Array.isArray(expr)) {
150
+ styleFilter.push('any', ...expr.map(e => ['==', key, e]))
151
+ } else {
152
+ styleFilter.push('==', key, expr)
153
+ }
154
+ }
155
+ }
156
+ return styleFilter
157
+ }
158
+
159
+ #match(properties, filter)
160
+ //========================
161
+ {
162
+ if (filter.constructor !== Object) {
163
+ return !!filter
164
+ }
165
+ for (const [key, expr] of Object.entries(filter)) {
166
+ let matched = true
167
+ if (key === 'AND' || key === 'OR') {
168
+ if (Array.isArray(expr) && expr.length >= 2) {
169
+ const matches = expr.map(e => this.#match(properties, e))
170
+ matched = (key === 'AND') ? matches.reduce((result, match) => result && match, true)
171
+ : matches.reduce((result, match) => result || match, false)
172
+ } else {
173
+ console.warn(`makeFilter: Invalid ${key} operands: ${expr}`)
174
+ }
175
+ } else if (key === 'HAS') {
176
+ matched = (expr in properties)
177
+ } else if (key === 'NOT') {
178
+ matched = !this.#match(properties, expr)
179
+ } else if (key in properties) {
180
+ const value = properties[key]
181
+ if (Array.isArray(value)) {
182
+ if (Array.isArray(expr)) {
183
+ matched = !(new Set(value).isDisjointFrom(new Set(expr)))
184
+ } else {
185
+ matched = value.includes(expr)
186
+ }
187
+ } else if (Array.isArray(expr)) {
188
+ matched = expr.includes(value)
189
+ } else {
190
+ matched = (value === expr)
191
+ }
192
+ }
193
+ if (!matched) {
194
+ return false
195
+ }
196
+ }
197
+ return true
198
+ }
199
+ }
200
+
201
+ //==============================================================================
202
+
203
+ const testProperties = {
204
+ prop: 1,
205
+ prop1: 5,
206
+ prop2: 11,
207
+ }
208
+
209
+ function testFilter(filter)
210
+ //=========================
211
+ {
212
+ const featureFilter = new PropertiesFilter(filter)
213
+ console.log(filter, '--->', featureFilter.makeStyleFilter(), featureFilter.match(testProperties))
214
+ }
215
+
216
+ function testFilters()
217
+ //====================
218
+ {
219
+ /*
220
+ { HAS: 'prop' } ---> [ 'has', 'prop' ]
221
+ { prop: 1 } ---> [ '==', 'prop', 1 ]
222
+ { NOT: { prop: 1 } } ---> [ '!=', 'prop', 1 ]
223
+ { NOT: { prop: [ 1, 2 ] } } ---> [ '!', [ 'any', [ '==', 'prop', 1 ], [ '==', 'prop', 2 ] ] ]
224
+ { OR: [ { prop1: 10 }, { prop2: 11 } ] } ---> [ 'any', [ '==', 'prop1', 10 ], [ '==', 'prop2', 11 ] ]
225
+ { AND: [ { prop1: 10 }, { prop2: 11 } ] } ---> [ 'all', [ '==', 'prop1', 10 ], [ '==', 'prop2', 11 ] ]
226
+ { OR: [ { AND: [Array] }, { AND: [Array] } ] } ---> [
227
+ 'any',
228
+ [ 'all', [ '!=', 'prop1', 10 ], [ '==', 'prop2', 11 ] ],
229
+ [ 'all', [ '==', 'prop3', 10 ], [ '==', 'prop4', 11 ] ]
230
+ ]
231
+ { NOT: { OR: [ [Object], [Object] ] } } ---> [
232
+ '!',
233
+ [ 'any', [ 'all', [Array], [Array] ], [ 'all', [Array], [Array] ] ]
234
+ ]
235
+ */
236
+
237
+ console.log('test properties', testProperties)
238
+
239
+ testFilter({
240
+ "HAS": "prop"
241
+ })
242
+
243
+ testFilter({
244
+ "prop": 1
245
+ })
246
+
247
+ testFilter({
248
+ "NOT": {
249
+ "prop": 1
250
+ }
251
+ })
252
+
253
+ testFilter({
254
+ "prop": [1, 2]
255
+ })
256
+
257
+ testFilter({
258
+ "NOT": {
259
+ "prop": [1, 2]
260
+ }
261
+ })
262
+
263
+ testFilter({
264
+ "OR": [
265
+ {"prop1": 10},
266
+ {"prop2": 11}
267
+ ]
268
+ })
269
+
270
+ testFilter({
271
+ "AND": [
272
+ {"prop1": 10},
273
+ {"prop2": 11}
274
+ ]
275
+ })
276
+
277
+ testFilter({
278
+ "OR": [{
279
+ "AND": [
280
+ { "NOT": {"prop1": 10}},
281
+ {"prop2": 11}
282
+ ]}, {
283
+ "AND": [
284
+ {"prop3": 10},
285
+ {"prop4": 11}
286
+ ]}
287
+ ]
288
+ })
289
+
290
+ testFilter({
291
+ "NOT": {
292
+ "OR": [{
293
+ "AND": [
294
+ {"prop1": 10},
295
+ {"prop2": 11}
296
+ ]}, {
297
+ "AND": [
298
+ {"prop3": 10},
299
+ {"prop4": 11}
300
+ ]}
301
+ ]
302
+ }
303
+ })
304
+ }
305
+
306
+ //==============================================================================
307
+
308
+ //testFilters()
309
+
310
+ //==============================================================================
@@ -27,6 +27,9 @@ import * as utils from '../utils.js';
27
27
 
28
28
  import * as style from './styling.js';
29
29
 
30
+ import {Paths3DLayer} from './paths3d'
31
+ import {PropertiesFilter} from './filter'
32
+
30
33
  const FEATURES_LAYER = 'features';
31
34
  const RASTER_LAYERS_NAME = 'Background image layer';
32
35
  const RASTER_LAYERS_ID = 'background-image-layer';
@@ -64,6 +67,12 @@ class MapStylingLayers
64
67
  return this.__active;
65
68
  }
66
69
 
70
+ get map()
71
+ //=======
72
+ {
73
+ return this.__map
74
+ }
75
+
67
76
  addLayer(styleLayer, options)
68
77
  //===========================
69
78
  {
@@ -106,6 +115,8 @@ class MapStylingLayers
106
115
 
107
116
  class MapFeatureLayers extends MapStylingLayers
108
117
  {
118
+ #pathLayers = []
119
+
109
120
  constructor(flatmap, layer, options)
110
121
  {
111
122
  super(flatmap, layer, options);
@@ -137,35 +148,46 @@ class MapFeatureLayers extends MapStylingLayers
137
148
  this.setPaint(this.__layerOptions);
138
149
  }
139
150
 
140
- __addStyleLayer(styleClass, sourceLayer=FEATURES_LAYER)
141
- //=====================================================
151
+ __addStyleLayer(styleClass, sourceLayer=FEATURES_LAYER, path2dLayer=false)
152
+ //========================================================================
142
153
  {
143
154
  const styleLayer = new styleClass(`${this.__id}_${sourceLayer}`,
144
- this.vectorSourceId(sourceLayer));
145
- this.addLayer(styleLayer, this.__layerOptions);
155
+ this.vectorSourceId(sourceLayer))
156
+ this.addLayer(styleLayer, this.__layerOptions)
157
+ if (path2dLayer) {
158
+ this.#pathLayers.push(styleLayer)
159
+ }
146
160
  }
147
161
 
148
162
  __addPathwayStyleLayers()
149
163
  //=======================
150
164
  {
151
- const pathwaysVectorSource = this.vectorSourceId(PATHWAYS_LAYER);
165
+ const pathwaysVectorSource = this.vectorSourceId(PATHWAYS_LAYER)
152
166
  if (this.__map.getSource('vector-tiles')
153
167
  .vectorLayerIds
154
168
  .includes(pathwaysVectorSource)) {
155
- this.__addStyleLayer(style.AnnotatedPathLayer, PATHWAYS_LAYER);
169
+ this.__addStyleLayer(style.AnnotatedPathLayer, PATHWAYS_LAYER, true)
156
170
 
157
- this.__addStyleLayer(style.CentrelineEdgeLayer, PATHWAYS_LAYER);
158
- this.__addStyleLayer(style.CentrelineTrackLayer, PATHWAYS_LAYER);
171
+ this.__addStyleLayer(style.CentrelineEdgeLayer, PATHWAYS_LAYER)
172
+ this.__addStyleLayer(style.CentrelineTrackLayer, PATHWAYS_LAYER)
159
173
 
160
- this.__addStyleLayer(style.PathLineLayer, PATHWAYS_LAYER);
161
- this.__addStyleLayer(style.PathDashlineLayer, PATHWAYS_LAYER);
174
+ this.__addStyleLayer(style.PathLineLayer, PATHWAYS_LAYER, true)
175
+ this.__addStyleLayer(style.PathDashlineLayer, PATHWAYS_LAYER, true)
162
176
 
163
- this.__addStyleLayer(style.NervePolygonBorder, PATHWAYS_LAYER);
164
- this.__addStyleLayer(style.NervePolygonFill, PATHWAYS_LAYER);
165
- this.__addStyleLayer(style.FeatureNerveLayer, PATHWAYS_LAYER);
177
+ this.__addStyleLayer(style.NervePolygonBorder, PATHWAYS_LAYER, true)
178
+ this.__addStyleLayer(style.NervePolygonFill, PATHWAYS_LAYER, true)
179
+ this.__addStyleLayer(style.FeatureNerveLayer, PATHWAYS_LAYER, true)
166
180
 
167
- this.__addStyleLayer(style.PathHighlightLayer, PATHWAYS_LAYER);
168
- this.__addStyleLayer(style.PathDashHighlightLayer, PATHWAYS_LAYER);
181
+ this.__addStyleLayer(style.PathHighlightLayer, PATHWAYS_LAYER, true)
182
+ this.__addStyleLayer(style.PathDashHighlightLayer, PATHWAYS_LAYER, true)
183
+ }
184
+ }
185
+
186
+ enablePaths2dLayer(visible)
187
+ //=========================
188
+ {
189
+ for (const layer of this.#pathLayers) {
190
+ this.map.setLayoutProperty(layer.id, 'visibility', visible ? 'visible' : 'none')
169
191
  }
170
192
  }
171
193
 
@@ -243,11 +265,14 @@ class MapRasterLayers extends MapStylingLayers
243
265
 
244
266
  export class LayerManager
245
267
  {
246
- constructor(flatmap)
268
+ #featureLayers = new Map()
269
+ #paths3dLayer = null
270
+ #rasterLayer = null
271
+
272
+ constructor(flatmap, ui)
247
273
  {
248
274
  this.__flatmap = flatmap;
249
275
  this.__map = flatmap.map;
250
- this.__mapLayers = new Map;
251
276
  this.__layerOptions = utils.setDefaults(flatmap.options.layerOptions, {
252
277
  colour: true,
253
278
  outline: true,
@@ -266,28 +291,37 @@ export class LayerManager
266
291
 
267
292
  // Image layers are below all feature layers
268
293
  const bodyLayer = flatmap.layers[0];
269
- const rasterLayers = new MapRasterLayers(this.__flatmap,
294
+ this.#rasterLayer = new MapRasterLayers(this.__flatmap,
270
295
  this.__layerOptions,
271
296
  bodyLayer.id); // body layer if not FC??
272
297
  for (const layer of flatmap.layers) {
273
- rasterLayers.addLayer(layer);
298
+ this.#rasterLayer.addLayer(layer);
274
299
  }
275
- this.__mapLayers.set(RASTER_LAYERS_ID, rasterLayers);
276
300
  } else {
277
301
  this.__layerOptions.activeRasterLayer = false;
278
302
  }
279
303
  for (const layer of flatmap.layers) {
280
- this.__mapLayers.set(layer.id, new MapFeatureLayers(this.__flatmap,
281
- layer,
282
- this.__layerOptions));
304
+ this.#featureLayers.set(layer.id, new MapFeatureLayers(this.__flatmap,
305
+ layer,
306
+ this.__layerOptions));
283
307
  }
308
+
309
+ // Support 3D path view
310
+ this.#paths3dLayer = new Paths3DLayer(flatmap, ui)
284
311
  }
285
312
 
286
313
  get layers()
287
314
  //==========
288
315
  {
289
- const layers = [];
290
- for (const mapLayer of this.__mapLayers.values()) {
316
+ const layers = []
317
+ if (this.#rasterLayer) {
318
+ layers.push({
319
+ id: this.#rasterLayer.id,
320
+ description: this.#rasterLayer.description,
321
+ enabled: this.#rasterLayer.active
322
+ })
323
+ }
324
+ for (const mapLayer of this.#featureLayers.values()) {
291
325
  layers.push({
292
326
  id: mapLayer.id,
293
327
  description: mapLayer.description,
@@ -306,26 +340,63 @@ export class LayerManager
306
340
  activate(layerId, enable=true)
307
341
  //============================
308
342
  {
309
- const layer = this.__mapLayers.get(layerId);
310
- if (layer !== undefined) {
311
- layer.activate(enable);
312
- if (layer.id === RASTER_LAYERS_ID) {
313
- this.__layerOptions.activeRasterLayer = enable;
314
- for (const mapLayer of this.__mapLayers.values()) {
315
- if (mapLayer.id !== RASTER_LAYERS_ID) {
316
- mapLayer.setPaint(this.__layerOptions);
317
- }
343
+ if (layerId === RASTER_LAYERS_ID) {
344
+ if (this.#rasterLayer) {
345
+ this.#rasterLayer.activate(enable)
346
+ this.__layerOptions.activeRasterLayer = enable
347
+ for (const mapLayer of this.#featureLayers.values()) {
348
+ mapLayer.setPaint(this.__layerOptions)
318
349
  }
319
350
  }
351
+ } else {
352
+ const layer = this.#featureLayers.get(layerId)
353
+ if (layer) {
354
+ layer.activate(enable)
355
+ }
356
+ }
357
+ }
358
+
359
+ featuresAtPoint(point)
360
+ //====================
361
+ {
362
+ let features = []
363
+ if (this.#paths3dLayer) {
364
+ features = this.#paths3dLayer.queryFeaturesAtPoint(point)
365
+ }
366
+ if (features.length === 0) {
367
+ features = this.__map.queryRenderedFeatures(point)
368
+ }
369
+ return features
370
+ }
371
+
372
+ removeFeatureState(feature, key)
373
+ //==============================
374
+ {
375
+ if (this.#paths3dLayer) {
376
+ this.#paths3dLayer.removeFeatureState(feature.id, key)
377
+ }
378
+ }
379
+
380
+ setFeatureState(feature, state)
381
+ //=============================
382
+ {
383
+ if (this.#paths3dLayer) {
384
+ this.#paths3dLayer.setFeatureState(feature.id, state)
320
385
  }
321
386
  }
322
387
 
323
388
  setPaint(options={})
324
389
  //==================
325
390
  {
326
- this.__layerOptions = utils.setDefaults(options, this.__layerOptions);
327
- for (const mapLayer of this.__mapLayers.values()) {
328
- mapLayer.setPaint(this.__layerOptions);
391
+ this.__layerOptions = utils.setDefaults(options, this.__layerOptions)
392
+ if (this.#rasterLayer) {
393
+ this.#rasterLayer.setPaint(this.__layerOptions)
394
+ }
395
+ for (const mapLayer of this.#featureLayers.values()) {
396
+ mapLayer.setPaint(this.__layerOptions)
397
+ }
398
+ if (this.#paths3dLayer) {
399
+ this.#paths3dLayer.setPaint(options)
329
400
  }
330
401
  }
331
402
 
@@ -333,9 +404,32 @@ export class LayerManager
333
404
  //===================
334
405
  {
335
406
  this.__layerOptions = utils.setDefaults(options, this.__layerOptions);
336
- for (const mapLayer of this.__mapLayers.values()) {
407
+ for (const mapLayer of this.#featureLayers.values()) {
337
408
  mapLayer.setFilter(this.__layerOptions);
338
409
  }
410
+ if (this.#paths3dLayer) {
411
+ const sckanState = options.sckan || 'valid'
412
+ const sckanFilter = (sckanState == 'none') ? {NOT: {HAS: 'sckan'}} :
413
+ (sckanState == 'valid') ? {sckan: true} :
414
+ (sckanState == 'invalid') ? {NOT: {sckan: true}} :
415
+ true
416
+ const featureFilter = new PropertiesFilter(sckanFilter)
417
+ if ('taxons' in options) {
418
+ featureFilter.narrow({taxons: options.taxons})
419
+ }
420
+ this.#paths3dLayer.setFilter(featureFilter)
421
+ }
422
+ }
423
+
424
+ set3dMode(enable=true)
425
+ //====================
426
+ {
427
+ if (this.#paths3dLayer) {
428
+ this.#paths3dLayer.enable(enable)
429
+ for (const mapLayer of this.#featureLayers.values()) {
430
+ mapLayer.enablePaths2dLayer(!enable)
431
+ }
432
+ }
339
433
  }
340
434
 
341
435
  enableSckanPaths(sckanState, enable=true)
@@ -26,6 +26,7 @@ import GL from '@luma.gl/constants'
26
26
  //==============================================================================
27
27
 
28
28
  import {pathColourArray} from '../pathways'
29
+ import {PropertiesFilter} from './filter'
29
30
 
30
31
  //==============================================================================
31
32
 
@@ -60,6 +61,7 @@ class ArcMapLayer extends ArcLayer
60
61
  shaders.vs = `#version 300 es\n${shaders.vs}`
61
62
  return shaders
62
63
  }
64
+
63
65
  setDataProperty(featureId, key, enabled)
64
66
  //======================================
65
67
  {
@@ -139,10 +141,12 @@ export class Paths3DLayer
139
141
  #deckOverlay = null
140
142
  #dimmed = false
141
143
  #enabled = false
144
+ #featureFilter = new PropertiesFilter()
142
145
  #featureToLayer = new Map()
143
146
  #knownTypes = []
144
147
  #map
145
148
  #pathData
149
+ #pathFilters
146
150
  #pathManager
147
151
  #pathStyles
148
152
  #ui
@@ -162,7 +166,27 @@ export class Paths3DLayer
162
166
  && 'pathEndPosition' in ann)
163
167
  .map(ann => [ann.featureId, ann]))
164
168
  this.#pathStyles = new Map(this.#pathManager.pathStyles().map(s => [s.type, s]))
165
- this.#knownTypes = [...this.#pathStyles.keys()].filter(t => t !== 'other')
169
+ const knownTypes = [...this.#pathStyles.keys()].filter(t => t !== 'other')
170
+ this.#pathFilters = new Map(
171
+ [...this.#pathStyles.keys()]
172
+ .map(pathType => [pathType, new PropertiesFilter({
173
+ OR: [{
174
+ AND: [
175
+ {kind: knownTypes},
176
+ {kind: pathType}
177
+ ],
178
+ },
179
+ {
180
+ AND: [
181
+ {
182
+ NOT: {kind: knownTypes}
183
+ },
184
+ (pathType === 'other')
185
+ ]
186
+ }]
187
+ })
188
+ ])
189
+ )
166
190
  }
167
191
 
168
192
  enable(enable=true)
@@ -223,6 +247,30 @@ export class Paths3DLayer
223
247
  }
224
248
  }
225
249
 
250
+ setFilter(featureFilter)
251
+ //======================
252
+ {
253
+ this.#featureFilter = featureFilter
254
+ if (this.#deckOverlay) {
255
+ const updatedLayers = new Map()
256
+ for (const [pathType, layer] of this.#arcLayers.entries()) {
257
+ layer.featureIds.forEach(id => this.#featureToLayer.delete(+id))
258
+ const pathStyle = this.#pathStyles.get(pathType)
259
+ if (pathStyle) {
260
+ const updatedLayer = pathStyle.dashed
261
+ ? new ArcDashedLayer(this.#layerOptions(pathType))
262
+ : new ArcMapLayer(this.#layerOptions(pathType))
263
+ updatedLayer.featureIds.forEach(id => this.#featureToLayer.set(+id, layer))
264
+ updatedLayers.set(pathType, updatedLayer)
265
+ }
266
+ }
267
+ this.#arcLayers = updatedLayers
268
+ this.#deckOverlay.setProps({
269
+ layers: [...this.#arcLayers.values()]
270
+ })
271
+ }
272
+ }
273
+
226
274
  setPaint(options)
227
275
  //===============
228
276
  {
@@ -236,11 +284,14 @@ export class Paths3DLayer
236
284
  #addArcLayer(pathType)
237
285
  //====================
238
286
  {
239
- const layer = this.#pathStyles.get(pathType).dashed
240
- ? new ArcDashedLayer(this.#layerOptions(pathType))
241
- : new ArcMapLayer(this.#layerOptions(pathType))
242
- layer.featureIds.forEach(id => this.#featureToLayer.set(+id, layer))
243
- this.#arcLayers.set(pathType, layer)
287
+ const pathStyle = this.#pathStyles.get(pathType)
288
+ if (pathStyle) {
289
+ const layer = pathStyle.dashed
290
+ ? new ArcDashedLayer(this.#layerOptions(pathType))
291
+ : new ArcMapLayer(this.#layerOptions(pathType))
292
+ layer.featureIds.forEach(id => this.#featureToLayer.set(+id, layer))
293
+ this.#arcLayers.set(pathType, layer)
294
+ }
244
295
  }
245
296
 
246
297
  #removeArcLayer(pathType)
@@ -283,13 +334,12 @@ export class Paths3DLayer
283
334
  }
284
335
  }
285
336
 
286
-
287
337
  #layerOptions(pathType)
288
338
  //=====================
289
339
  {
290
- const pathData = [...this.#pathData.values()]
291
- .filter(ann => (this.#knownTypes.includes(ann.kind) && (ann.kind === pathType)
292
- || !this.#knownTypes.includes(ann.kind) && (pathType === 'other')))
340
+ const filter = this.#pathFilters.get(pathType)
341
+ const pathData = (filter ? [...this.#pathData.values()].filter(ann => filter.match(ann))
342
+ : []).filter(ann => this.#featureFilter.match(ann))
293
343
  return {
294
344
  id: `arc-${pathType}`,
295
345
  data: pathData,