@abi-software/flatmap-viewer 2.6.0-a.1 → 2.6.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.
@@ -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
+ //==============================================================================
@@ -22,13 +22,14 @@ limitations under the License.
22
22
 
23
23
  //==============================================================================
24
24
 
25
- import {FeatureFilter} from './filter'
26
-
27
25
  import {PATHWAYS_LAYER} from '../pathways.js';
28
26
  import * as utils from '../utils.js';
29
27
 
30
28
  import * as style from './styling.js';
31
29
 
30
+ import {Paths3DLayer} from './paths3d'
31
+ import {PropertiesFilter} from './filter'
32
+
32
33
  const FEATURES_LAYER = 'features';
33
34
  const RASTER_LAYERS_NAME = 'Background image layer';
34
35
  const RASTER_LAYERS_ID = 'background-image-layer';
@@ -46,11 +47,6 @@ class MapStylingLayers
46
47
  this.__layers = [];
47
48
  this.__layerOptions = options;
48
49
  this.__separateLayers = flatmap.options.separateLayers;
49
-
50
- const f = new FeatureFilter({
51
- "HAS": "prop"
52
- })
53
- console.log(f.makeStyleFilter())
54
50
  }
55
51
 
56
52
  get id()
@@ -71,6 +67,12 @@ class MapStylingLayers
71
67
  return this.__active;
72
68
  }
73
69
 
70
+ get map()
71
+ //=======
72
+ {
73
+ return this.__map
74
+ }
75
+
74
76
  addLayer(styleLayer, options)
75
77
  //===========================
76
78
  {
@@ -113,6 +115,8 @@ class MapStylingLayers
113
115
 
114
116
  class MapFeatureLayers extends MapStylingLayers
115
117
  {
118
+ #pathLayers = []
119
+
116
120
  constructor(flatmap, layer, options)
117
121
  {
118
122
  super(flatmap, layer, options);
@@ -144,35 +148,46 @@ class MapFeatureLayers extends MapStylingLayers
144
148
  this.setPaint(this.__layerOptions);
145
149
  }
146
150
 
147
- __addStyleLayer(styleClass, sourceLayer=FEATURES_LAYER)
148
- //=====================================================
151
+ __addStyleLayer(styleClass, sourceLayer=FEATURES_LAYER, path2dLayer=false)
152
+ //========================================================================
149
153
  {
150
154
  const styleLayer = new styleClass(`${this.__id}_${sourceLayer}`,
151
- this.vectorSourceId(sourceLayer));
152
- this.addLayer(styleLayer, this.__layerOptions);
155
+ this.vectorSourceId(sourceLayer))
156
+ this.addLayer(styleLayer, this.__layerOptions)
157
+ if (path2dLayer) {
158
+ this.#pathLayers.push(styleLayer)
159
+ }
153
160
  }
154
161
 
155
162
  __addPathwayStyleLayers()
156
163
  //=======================
157
164
  {
158
- const pathwaysVectorSource = this.vectorSourceId(PATHWAYS_LAYER);
165
+ const pathwaysVectorSource = this.vectorSourceId(PATHWAYS_LAYER)
159
166
  if (this.__map.getSource('vector-tiles')
160
167
  .vectorLayerIds
161
168
  .includes(pathwaysVectorSource)) {
162
- this.__addStyleLayer(style.AnnotatedPathLayer, PATHWAYS_LAYER);
169
+ this.__addStyleLayer(style.AnnotatedPathLayer, PATHWAYS_LAYER, true)
163
170
 
164
- this.__addStyleLayer(style.CentrelineEdgeLayer, PATHWAYS_LAYER);
165
- this.__addStyleLayer(style.CentrelineTrackLayer, PATHWAYS_LAYER);
171
+ this.__addStyleLayer(style.CentrelineEdgeLayer, PATHWAYS_LAYER)
172
+ this.__addStyleLayer(style.CentrelineTrackLayer, PATHWAYS_LAYER)
166
173
 
167
- this.__addStyleLayer(style.PathLineLayer, PATHWAYS_LAYER);
168
- this.__addStyleLayer(style.PathDashlineLayer, PATHWAYS_LAYER);
174
+ this.__addStyleLayer(style.PathLineLayer, PATHWAYS_LAYER, true)
175
+ this.__addStyleLayer(style.PathDashlineLayer, PATHWAYS_LAYER, true)
169
176
 
170
- this.__addStyleLayer(style.NervePolygonBorder, PATHWAYS_LAYER);
171
- this.__addStyleLayer(style.NervePolygonFill, PATHWAYS_LAYER);
172
- 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)
173
180
 
174
- this.__addStyleLayer(style.PathHighlightLayer, PATHWAYS_LAYER);
175
- 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')
176
191
  }
177
192
  }
178
193
 
@@ -250,11 +265,14 @@ class MapRasterLayers extends MapStylingLayers
250
265
 
251
266
  export class LayerManager
252
267
  {
253
- constructor(flatmap)
268
+ #featureLayers = new Map()
269
+ #paths3dLayer = null
270
+ #rasterLayer = null
271
+
272
+ constructor(flatmap, ui)
254
273
  {
255
274
  this.__flatmap = flatmap;
256
275
  this.__map = flatmap.map;
257
- this.__mapLayers = new Map;
258
276
  this.__layerOptions = utils.setDefaults(flatmap.options.layerOptions, {
259
277
  colour: true,
260
278
  outline: true,
@@ -273,28 +291,37 @@ export class LayerManager
273
291
 
274
292
  // Image layers are below all feature layers
275
293
  const bodyLayer = flatmap.layers[0];
276
- const rasterLayers = new MapRasterLayers(this.__flatmap,
294
+ this.#rasterLayer = new MapRasterLayers(this.__flatmap,
277
295
  this.__layerOptions,
278
296
  bodyLayer.id); // body layer if not FC??
279
297
  for (const layer of flatmap.layers) {
280
- rasterLayers.addLayer(layer);
298
+ this.#rasterLayer.addLayer(layer);
281
299
  }
282
- this.__mapLayers.set(RASTER_LAYERS_ID, rasterLayers);
283
300
  } else {
284
301
  this.__layerOptions.activeRasterLayer = false;
285
302
  }
286
303
  for (const layer of flatmap.layers) {
287
- this.__mapLayers.set(layer.id, new MapFeatureLayers(this.__flatmap,
288
- layer,
289
- this.__layerOptions));
304
+ this.#featureLayers.set(layer.id, new MapFeatureLayers(this.__flatmap,
305
+ layer,
306
+ this.__layerOptions));
290
307
  }
308
+
309
+ // Support 3D path view
310
+ this.#paths3dLayer = new Paths3DLayer(flatmap, ui)
291
311
  }
292
312
 
293
313
  get layers()
294
314
  //==========
295
315
  {
296
- const layers = [];
297
- 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()) {
298
325
  layers.push({
299
326
  id: mapLayer.id,
300
327
  description: mapLayer.description,
@@ -313,26 +340,63 @@ export class LayerManager
313
340
  activate(layerId, enable=true)
314
341
  //============================
315
342
  {
316
- const layer = this.__mapLayers.get(layerId);
317
- if (layer !== undefined) {
318
- layer.activate(enable);
319
- if (layer.id === RASTER_LAYERS_ID) {
320
- this.__layerOptions.activeRasterLayer = enable;
321
- for (const mapLayer of this.__mapLayers.values()) {
322
- if (mapLayer.id !== RASTER_LAYERS_ID) {
323
- mapLayer.setPaint(this.__layerOptions);
324
- }
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)
325
349
  }
326
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)
327
385
  }
328
386
  }
329
387
 
330
388
  setPaint(options={})
331
389
  //==================
332
390
  {
333
- this.__layerOptions = utils.setDefaults(options, this.__layerOptions);
334
- for (const mapLayer of this.__mapLayers.values()) {
335
- 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)
336
400
  }
337
401
  }
338
402
 
@@ -340,9 +404,32 @@ export class LayerManager
340
404
  //===================
341
405
  {
342
406
  this.__layerOptions = utils.setDefaults(options, this.__layerOptions);
343
- for (const mapLayer of this.__mapLayers.values()) {
407
+ for (const mapLayer of this.#featureLayers.values()) {
344
408
  mapLayer.setFilter(this.__layerOptions);
345
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
+ }
346
433
  }
347
434
 
348
435
  enableSckanPaths(sckanState, enable=true)