@abi-software/flatmap-viewer 2.3.0-a.6 → 2.3.0-b.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
@@ -38,7 +38,7 @@ The map server endpoint is specified as ``MAP_ENDPOINT`` in ``src/main.js``. It
38
38
  Package Installation
39
39
  ====================
40
40
 
41
- * ``npm install @abi-software/flatmap-viewer@2.3.0-a.6``
41
+ * ``npm install @abi-software/flatmap-viewer@2.3.0-b.1``
42
42
 
43
43
  Documentation
44
44
  -------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abi-software/flatmap-viewer",
3
- "version": "2.3.0-a.6",
3
+ "version": "2.3.0-b.1",
4
4
  "description": "Flatmap viewer using Maplibre GL",
5
5
  "repository": "https://github.com/AnatomicMaps/flatmap-viewer.git",
6
6
  "main": "src/main.js",
package/src/annotation.js CHANGED
@@ -119,8 +119,8 @@ export class Annotator
119
119
  /*
120
120
  const testUser = {name: 'Testing...'};
121
121
  this.__setUser(testUser);
122
- callback(testUser);
123
-
122
+ this.__authorised = true;
123
+ return Promise.resolve(testUser);
124
124
  */
125
125
  const abortController = new AbortController();
126
126
  setTimeout((panel) => {
@@ -147,7 +147,7 @@ export class Annotator
147
147
  } else {
148
148
  this.__setUser(user_data);
149
149
  this.__authorised = true;
150
- return user_data;
150
+ return Promise.resolve(user_data);
151
151
  }
152
152
  } else {
153
153
  return Promise.resolve({error: `${response.status} ${response.statusText}`});
@@ -174,7 +174,7 @@ export class Annotator
174
174
  });
175
175
  if (response.ok) {
176
176
  this.__authorised = false;
177
- return await response.json();
177
+ return response.json();
178
178
  } else {
179
179
  return Promise.resolve({error: `${response.status} ${response.statusText}`});
180
180
  }
@@ -307,8 +307,8 @@ export class Annotator
307
307
  listValues.push(inputField.value.trim());
308
308
  }
309
309
  const lastValue = field.update ? provenanceData[field.key] || [] : [];
310
- const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort();
311
- const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort();
310
+ const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort(Intl.Collator().compare);
311
+ const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort(Intl.Collator().compare);
312
312
  if (oldValues.length !== newValues.length
313
313
  || oldValues.filter(v => !newValues.includes(v)).length > 0) {
314
314
  newProperties[field.key] = newValues;
@@ -336,7 +336,7 @@ export class Annotator
336
336
  }
337
337
  }, UPDATE_TIMEOUT, panel);
338
338
 
339
- const url = this.__flatmap.addBaseUrl_(`/annotations/${this.__currentFeatureId}`);
339
+ const url = this.__flatmap.makeServerUrl(this.__currentFeatureId, 'annotator/');
340
340
  const response = await fetch(url, {
341
341
  headers: { "Content-Type": "application/json; charset=utf-8" },
342
342
  method: 'POST',
@@ -344,7 +344,7 @@ export class Annotator
344
344
  signal: abortController.signal
345
345
  });
346
346
  if (response.ok) {
347
- return await response.json();
347
+ return response.json();
348
348
  } else {
349
349
  return Promise.resolve({error: `${response.status} ${response.statusText}`});
350
350
  }
@@ -367,6 +367,7 @@ export class Annotator
367
367
  if ('error' in response) {
368
368
  this.__setStatusMessage(response.error);
369
369
  } else {
370
+ this.__flatmap.setFeatureAnnotated(this.__currentFeatureId);
370
371
  panel.close();
371
372
  }
372
373
  } else {
@@ -490,7 +491,7 @@ export class Annotator
490
491
  '<span id="flatmap-annotation-lock" class="jsPanel-ftr-btn fa fa-lock"></span>',
491
492
  ],
492
493
  contentFetch: {
493
- resource: flatmap.addBaseUrl_(`/annotations/${this.__currentFeatureId}`),
494
+ resource: flatmap.makeServerUrl(this.__currentFeatureId, 'annotator/'),
494
495
  fetchInit: {
495
496
  method: 'GET',
496
497
  mode: 'cors',
@@ -525,6 +526,24 @@ export class Annotator
525
526
  document.addEventListener('jspanelclosed', closedCallback, false);
526
527
  }
527
528
 
529
+ async annotated_features()
530
+ //========================
531
+ {
532
+ const url = this.__flatmap.makeServerUrl('', 'annotator/');
533
+ const response = await fetch(url, {
534
+ headers: {
535
+ "Accept": "application/json; charset=utf-8",
536
+ "Cache-Control": "no-store"
537
+ }
538
+ });
539
+ if (response.ok) {
540
+ return response.json();
541
+ } else {
542
+ console.error(`Annotated features: ${response.status} ${response.statusText}`);
543
+ return Promise.resolve([]);
544
+ }
545
+ }
546
+
528
547
  }
529
548
 
530
549
  //==============================================================================
@@ -85,12 +85,12 @@ class FlatMap
85
85
 
86
86
  for (const [id, source] of Object.entries(mapDescription.style.sources)) {
87
87
  if (source.url) {
88
- source.url = this.addBaseUrl_(source.url);
88
+ source.url = this.makeServerUrl(source.url);
89
89
  }
90
90
  if (source.tiles) {
91
91
  const tiles = [];
92
92
  for (const tileUrl of source.tiles) {
93
- tiles.push(this.addBaseUrl_(tileUrl));
93
+ tiles.push(this.makeServerUrl(tileUrl));
94
94
  }
95
95
  source.tiles = tiles;
96
96
  }
@@ -361,21 +361,23 @@ class FlatMap
361
361
  {
362
362
  if (!this._map.hasImage(id)) {
363
363
  const image = await (path.startsWith('data:image') ? this.loadEncodedImage_(path)
364
- : this.loadImage_(path.startsWith('/') ? this.addBaseUrl_(path)
364
+ : this.loadImage_(path.startsWith('/') ? this.makeServerUrl(path)
365
365
  : new URL(path, baseUrl)));
366
366
  this._map.addImage(id, image, options);
367
367
  }
368
368
  }
369
369
 
370
- addBaseUrl_(url)
371
- //==============
370
+ makeServerUrl(url, resource='flatmap/')
371
+ //=====================================
372
372
  {
373
- if (url.startsWith('/')) {
374
- return `${this._baseUrl}flatmap/${this.__uuid}${url}`; // We don't want embedded `{` and `}` characters escaped
375
- } else if (!url.startsWith('http://') && !url.startsWith('https://')) {
376
- console.log(`Invalid URL (${url}) in map's sources`);
373
+ if (url.startsWith('http://') || url.startsWith('https://')) {
374
+ return url;
375
+ } else if (url.startsWith('/')) {
376
+ // We don't want embedded `{` and `}` characters escaped
377
+ return `${this._baseUrl}${resource}${this.__uuid}${url}`;
378
+ } else {
379
+ return `${this._baseUrl}${resource}${this.__uuid}/${url}`;
377
380
  }
378
- return url;
379
381
  }
380
382
 
381
383
  /**
@@ -470,6 +472,19 @@ class FlatMap
470
472
  return this.__idToAnnotation.get(featureId.toString());
471
473
  }
472
474
 
475
+ /**
476
+ * Flag the feature as having external annotation.
477
+ *
478
+ * @param {string} featureId The feature's identifier
479
+ */
480
+ setFeatureAnnotated(featureId)
481
+ //============================
482
+ {
483
+ if (this._userInteractions !== null) {
484
+ this._userInteractions.setFeatureAnnotated(featureId);
485
+ }
486
+ }
487
+
473
488
  __updateFeatureIdMap(property, featureIdMap, annotation)
474
489
  //======================================================
475
490
  {
@@ -144,14 +144,13 @@ export class UserInteractions
144
144
  }
145
145
  }
146
146
 
147
- // Flag features that have annotations and note which are FC systems
147
+ this.__featureIdToMapId = new Map();
148
+ this.__setupAnnotation();
149
+
150
+ // Note features that are FC systems
148
151
 
149
152
  this.__systems = new Map();
150
- for (const [id, ann] of flatmap.annotations) {
151
- const feature = this.mapFeature_(id);
152
- if (feature !== undefined) {
153
- this._map.setFeatureState(feature, { 'annotated': true });
154
- }
153
+ for (const [id, ann] of this._flatmap.annotations) {
155
154
  if (ann['fc-class'] === 'fc-class:System') {
156
155
  if (this.__systems.has(ann.name)) {
157
156
  this.__systems.get(ann.name).featureIds.push(ann.featureId)
@@ -197,10 +196,6 @@ export class UserInteractions
197
196
  }
198
197
  }
199
198
 
200
- // Add annotation capabilities
201
-
202
- this.__annotator = new Annotator(flatmap);
203
-
204
199
  // Handle mouse events
205
200
 
206
201
  this._map.on('click', this.clickEvent_.bind(this));
@@ -254,6 +249,39 @@ export class UserInteractions
254
249
  }
255
250
  }
256
251
 
252
+ async __setupAnnotation()
253
+ //=======================
254
+ {
255
+ // Add annotation capability
256
+
257
+ this.__annotator = new Annotator(this._flatmap);
258
+ const annotated_features = await this.__annotator.annotated_features();
259
+
260
+ // Flag features that have annotations
261
+
262
+ for (const [mapId, ann] of this._flatmap.annotations) {
263
+ this.__featureIdToMapId.set(ann.id, mapId);
264
+ const feature = this.mapFeature_(mapId);
265
+ if (feature !== undefined) {
266
+ this._map.setFeatureState(feature, { 'map-annotation': true });
267
+ if (annotated_features.indexOf(ann.id) >= 0) {
268
+ this._map.setFeatureState(feature, { 'annotated': true });
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ setFeatureAnnotated(featureId)
275
+ //============================
276
+ {
277
+ // featureId v's geoJSON id
278
+ const mapId = this.__featureIdToMapId.get(featureId);
279
+ const feature = this.mapFeature_(mapId);
280
+ if (feature !== undefined) {
281
+ this._map.setFeatureState(feature, { 'annotated': true });
282
+ }
283
+ }
284
+
257
285
  setPaint(options)
258
286
  //===============
259
287
  {
@@ -447,7 +475,7 @@ export class UserInteractions
447
475
  let smallestFeature = null;
448
476
  for (const feature of features) {
449
477
  if (feature.geometry.type.includes('Polygon')
450
- && this._map.getFeatureState(feature)['annotated']) {
478
+ && this._map.getFeatureState(feature)['map-annotation']) {
451
479
  const polygon = turf.geometry(feature.geometry.type, feature.geometry.coordinates);
452
480
  const area = turfArea(polygon);
453
481
  if (smallestFeature === null || smallestArea > area) {
package/src/layers.js CHANGED
@@ -154,6 +154,8 @@ class MapFeatureLayers extends MapStylingLayers
154
154
  if (this.__map.getSource('vector-tiles')
155
155
  .vectorLayerIds
156
156
  .indexOf(pathwaysVectorSource) >= 0) {
157
+ this.__addStyleLayer(style.AnnotatedPathLayer, PATHWAYS_LAYER);
158
+
157
159
  this.__addStyleLayer(style.CentrelineEdgeLayer, PATHWAYS_LAYER);
158
160
  this.__addStyleLayer(style.CentrelineTrackLayer, PATHWAYS_LAYER);
159
161
 
package/src/styling.js CHANGED
@@ -30,6 +30,20 @@ import {PATH_STYLE_RULES} from './pathways.js';
30
30
 
31
31
  //==============================================================================
32
32
 
33
+ const COLOUR_ACTIVE = 'blue';
34
+ const COLOUR_ANNOTATED = '#0F0';
35
+ const COLOUR_SELECTED = '#0F0';
36
+
37
+ const CENTRELINE_ACTIVE = '#444';
38
+ const CENTRELINE_COLOUR = '#CCC';
39
+
40
+ const FEATURE_SELECTED_BORDER = 'black';
41
+
42
+ const NERVE_ACTIVE = '#222';
43
+ const NERVE_SELECTED = 'red';
44
+
45
+ //==============================================================================
46
+
33
47
  class VectorStyleLayer
34
48
  {
35
49
  constructor(id, suffix, sourceLayer)
@@ -126,7 +140,7 @@ export class FeatureFillLayer extends VectorStyleLayer
126
140
  const paintStyle = {
127
141
  'fill-color': [
128
142
  'case',
129
- ['boolean', ['feature-state', 'selected'], false], '#0F0',
143
+ ['boolean', ['feature-state', 'selected'], false], COLOUR_SELECTED,
130
144
  ['has', 'colour'], ['get', 'colour'],
131
145
  ['boolean', ['feature-state', 'active'], false], coloured ? '#D88' : '#CCC',
132
146
  'white' // background colour? body colour ??
@@ -180,11 +194,13 @@ export class FeatureBorderLayer extends VectorStyleLayer
180
194
  const activeRasterLayer = 'activeRasterLayer' in options && options.activeRasterLayer;
181
195
  const lineColour = [ 'case' ];
182
196
  lineColour.push(['boolean', ['feature-state', 'selected'], false]);
183
- lineColour.push('black');
197
+ lineColour.push(FEATURE_SELECTED_BORDER);
184
198
  if (coloured && outlined) {
185
199
  lineColour.push(['boolean', ['feature-state', 'active'], false]);
186
- lineColour.push('blue');
200
+ lineColour.push(COLOUR_ACTIVE);
187
201
  }
202
+ lineColour.push(['boolean', ['feature-state', 'annotated'], false]);
203
+ lineColour.push(COLOUR_ANNOTATED);
188
204
  lineColour.push(['has', 'colour']);
189
205
  lineColour.push(['get', 'colour']);
190
206
  lineColour.push('#444');
@@ -199,6 +215,8 @@ export class FeatureBorderLayer extends VectorStyleLayer
199
215
  }
200
216
  lineOpacity.push(['boolean', ['feature-state', 'selected'], false]);
201
217
  lineOpacity.push(0.9);
218
+ lineOpacity.push(['boolean', ['feature-state', 'annotated'], false]);
219
+ lineOpacity.push(0.9);
202
220
  if (activeRasterLayer) {
203
221
  lineOpacity.push((outlined && !dimmed) ? 0.3 : 0.1);
204
222
  } else {
@@ -215,6 +233,8 @@ export class FeatureBorderLayer extends VectorStyleLayer
215
233
  lineWidth.push(['boolean', ['feature-state', 'active'], false]);
216
234
  lineWidth.push(1.5);
217
235
  }
236
+ lineWidth.push(['boolean', ['feature-state', 'annotated'], false]);
237
+ lineWidth.push(3.5);
218
238
  lineWidth.push(['has', 'colour']);
219
239
  lineWidth.push(0.7);
220
240
  lineWidth.push((coloured && outlined) ? 0.5 : 0.1);
@@ -276,7 +296,7 @@ export class FeatureLineLayer extends VectorStyleLayer
276
296
  const paintStyle = {
277
297
  'line-color': [
278
298
  'case',
279
- ['boolean', ['feature-state', 'selected'], false], '#0F0',
299
+ ['boolean', ['feature-state', 'selected'], false], COLOUR_SELECTED,
280
300
  ['boolean', ['feature-state', 'active'], false], coloured ? '#888' : '#CCC',
281
301
  ['has', 'colour'], ['get', 'colour'],
282
302
  ['==', ['get', 'type'], 'network'], '#AFA202',
@@ -338,6 +358,63 @@ export class FeatureDashLineLayer extends FeatureLineLayer
338
358
 
339
359
  //==============================================================================
340
360
 
361
+ export class AnnotatedPathLayer extends VectorStyleLayer
362
+ {
363
+ constructor(id, sourceLayer)
364
+ {
365
+ super(id, 'annotated-path', sourceLayer);
366
+ }
367
+
368
+ paintStyle(options={}, changes=false)
369
+ {
370
+ const dimmed = 'dimmed' in options && options.dimmed;
371
+ const paintStyle = {
372
+ 'line-color': COLOUR_ANNOTATED,
373
+ 'line-dasharray': [5, 0.5, 3, 0.5],
374
+ 'line-opacity': [
375
+ 'case',
376
+ ['boolean', ['feature-state', 'hidden'], false], 0.05,
377
+ ['boolean', ['feature-state', 'annotated'], false],
378
+ (dimmed ? 0.1 : 0.8),
379
+ 0.6
380
+ ],
381
+ 'line-width': [
382
+ 'let',
383
+ 'width',
384
+ ['case',
385
+ ['boolean', ['feature-state', 'annotated'], false],
386
+ ['*', 1.2, ['case', ['has', 'stroke-width'], ['get', 'stroke-width'], 1.0]],
387
+ 0.0
388
+ ],
389
+ ['interpolate',
390
+ ['exponential', 2],
391
+ ['zoom'],
392
+ 2, ["*", ['var', 'width'], ["^", 2, -0.5]],
393
+ 7, ["*", ['var', 'width'], ["^", 2, 2.5]],
394
+ 9, ["*", ['var', 'width'], ["^", 2, 4.0]]
395
+ ]
396
+ ]
397
+ };
398
+ return super.changedPaintStyle(paintStyle, changes);
399
+ }
400
+
401
+ style(options)
402
+ {
403
+ const dimmed = 'dimmed' in options && options.dimmed;
404
+ return {
405
+ ...super.style(),
406
+ 'type': 'line',
407
+ 'filter': ['==', '$type', 'LineString'],
408
+ 'paint': this.paintStyle(options),
409
+ 'layout': {
410
+ 'line-cap': 'square'
411
+ }
412
+ };
413
+ }
414
+ }
415
+
416
+ //==============================================================================
417
+
341
418
  export class PathLineLayer extends VectorStyleLayer
342
419
  {
343
420
  constructor(id, sourceLayer, options={})
@@ -402,7 +479,7 @@ export class PathLineLayer extends VectorStyleLayer
402
479
  const paintStyle = {
403
480
  'line-color': [
404
481
  'case',
405
- ['boolean', ['feature-state', 'selected'], false], '#0F0',
482
+ ['boolean', ['feature-state', 'selected'], false], COLOUR_SELECTED,
406
483
  ['boolean', ['feature-state', 'hidden'], false], '#CCC',
407
484
  ['==', ['get', 'type'], 'bezier'], 'red',
408
485
  ['==', ['get', 'kind'], 'unknown'], '#888',
@@ -487,9 +564,9 @@ class CentrelineLayer extends VectorStyleLayer
487
564
  const paintStyle = {
488
565
  'line-color': (this.__type == 'edge') ? '#000' : [
489
566
  'case',
490
- ['boolean', ['feature-state', 'selected'], false], '#0F0',
491
- ['boolean', ['feature-state', 'active'], false], '#444',
492
- '#CCC'
567
+ ['boolean', ['feature-state', 'selected'], false], COLOUR_SELECTED,
568
+ ['boolean', ['feature-state', 'active'], false], CENTRELINE_ACTIVE,
569
+ CENTRELINE_COLOUR
493
570
  ],
494
571
  'line-opacity': [
495
572
  'case',
@@ -650,8 +727,8 @@ export class FeatureNerveLayer extends VectorStyleLayer
650
727
  'line-color': [
651
728
  'case',
652
729
  ['boolean', ['feature-state', 'hidden'], false], '#CCC',
653
- ['boolean', ['feature-state', 'active'], false], '#222',
654
- ['boolean', ['feature-state', 'selected'], false], 'red',
730
+ ['boolean', ['feature-state', 'active'], false], NERVE_ACTIVE,
731
+ ['boolean', ['feature-state', 'selected'], false], NERVE_SELECTED,
655
732
  '#888'
656
733
  ],
657
734
  'line-opacity': [
@@ -702,7 +779,7 @@ export class NervePolygonBorder extends VectorStyleLayer
702
779
  'paint': {
703
780
  'line-color': [
704
781
  'case',
705
- ['boolean', ['feature-state', 'active'], false], 'blue',
782
+ ['boolean', ['feature-state', 'active'], false], COLOUR_ACTIVE,
706
783
  ['boolean', ['feature-state', 'selected'], false], 'red',
707
784
  '#444'
708
785
  ],