@abi-software/flatmap-viewer 2.4.2-b.5 → 2.4.3
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 +1 -1
- package/package.json +5 -6
- package/src/controls/controls.js +0 -67
- package/src/flatmap-viewer.js +0 -37
- package/src/interactions.js +2 -115
- package/src/main.js +0 -1
- package/src/styling.js +35 -48
- package/src/annotation.js +0 -665
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.4.
|
|
41
|
+
* ``npm install @abi-software/flatmap-viewer@2.4.3``
|
|
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.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "Flatmap viewer using Maplibre GL",
|
|
5
5
|
"repository": "https://github.com/AnatomicMaps/flatmap-viewer.git",
|
|
6
6
|
"main": "src/main.js",
|
|
@@ -19,14 +19,13 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/runtime": "^7.10.4",
|
|
21
21
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
|
22
|
-
"@turf/area": "^6.0
|
|
23
|
-
"@turf/bbox": "^6.0
|
|
24
|
-
"@turf/helpers": "^6.
|
|
22
|
+
"@turf/area": "^6.5.0",
|
|
23
|
+
"@turf/bbox": "^6.5.0",
|
|
24
|
+
"@turf/helpers": "^6.5.0",
|
|
25
25
|
"@turf/projection": "^6.5.0",
|
|
26
26
|
"bezier-js": "^6.1.0",
|
|
27
27
|
"html-es6cape": "^2.0.2",
|
|
28
|
-
"
|
|
29
|
-
"maplibre-gl": ">=3.0.0",
|
|
28
|
+
"maplibre-gl": ">=3.6.0",
|
|
30
29
|
"minisearch": "^2.2.1",
|
|
31
30
|
"polylabel": "^1.1.0"
|
|
32
31
|
},
|
package/src/controls/controls.js
CHANGED
|
@@ -526,73 +526,6 @@ export class NerveControl
|
|
|
526
526
|
|
|
527
527
|
//==============================================================================
|
|
528
528
|
|
|
529
|
-
export class AnnotatedControl
|
|
530
|
-
{
|
|
531
|
-
constructor(ui, options={excludeAnnotated: false})
|
|
532
|
-
{
|
|
533
|
-
this.__ui = ui;
|
|
534
|
-
this.__map = undefined;
|
|
535
|
-
this.__exclude = options.excludeAnnotated || false;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
getDefaultPosition()
|
|
539
|
-
//==================
|
|
540
|
-
{
|
|
541
|
-
return 'top-right';
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
onAdd(map)
|
|
545
|
-
//========
|
|
546
|
-
{
|
|
547
|
-
this.__map = map;
|
|
548
|
-
this.__container = document.createElement('div');
|
|
549
|
-
this.__container.className = 'maplibregl-ctrl';
|
|
550
|
-
|
|
551
|
-
this.__button = document.createElement('button');
|
|
552
|
-
this.__button.id = 'map-annotated-button';
|
|
553
|
-
this.__button.className = 'control-button text-button';
|
|
554
|
-
this.__button.setAttribute('type', 'button');
|
|
555
|
-
this.__button.setAttribute('aria-label', 'Show/hide annotated paths');
|
|
556
|
-
this.__button.textContent = 'UNANN';
|
|
557
|
-
this.__button.title = 'Show/hide annotated paths';
|
|
558
|
-
this.__container.appendChild(this.__button);
|
|
559
|
-
|
|
560
|
-
this.__container.addEventListener('click', this.onClick_.bind(this));
|
|
561
|
-
this.__setBackground();
|
|
562
|
-
return this.__container;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
__setBackground()
|
|
566
|
-
//===============
|
|
567
|
-
{
|
|
568
|
-
if (this.__exclude) {
|
|
569
|
-
this.__button.setAttribute('style', 'background: red');
|
|
570
|
-
} else {
|
|
571
|
-
this.__button.removeAttribute('style');
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
onRemove()
|
|
576
|
-
//========
|
|
577
|
-
{
|
|
578
|
-
this.__container.parentNode.removeChild(this.__container);
|
|
579
|
-
this.__map = undefined;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
onClick_(event)
|
|
583
|
-
//=============
|
|
584
|
-
{
|
|
585
|
-
if (event.target.id === 'map-annotated-button') {
|
|
586
|
-
this.__exclude = !this.__exclude;
|
|
587
|
-
this.__setBackground();
|
|
588
|
-
this.__ui.excludeAnnotated(this.__exclude);
|
|
589
|
-
}
|
|
590
|
-
event.stopPropagation();
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
//==============================================================================
|
|
595
|
-
|
|
596
529
|
export class BackgroundControl
|
|
597
530
|
{
|
|
598
531
|
constructor(flatmap)
|
package/src/flatmap-viewer.js
CHANGED
|
@@ -95,9 +95,6 @@ class FlatMap
|
|
|
95
95
|
this.__addAnnotation(featureId, annotation);
|
|
96
96
|
this.__searchIndex.indexMetadata(featureId, annotation);
|
|
97
97
|
}
|
|
98
|
-
if (this.options.annotator) {
|
|
99
|
-
this.__addAnnotatedComments();
|
|
100
|
-
}
|
|
101
98
|
|
|
102
99
|
// Set base of source URLs in map's style
|
|
103
100
|
|
|
@@ -223,25 +220,6 @@ class FlatMap
|
|
|
223
220
|
});
|
|
224
221
|
}
|
|
225
222
|
|
|
226
|
-
async __addAnnotatedComments()
|
|
227
|
-
//============================
|
|
228
|
-
{
|
|
229
|
-
const url = this.makeServerUrl('', 'annotator/')
|
|
230
|
-
const annotatedFeatures = await loadJSON(url);
|
|
231
|
-
for (const annotatedId of annotatedFeatures) {
|
|
232
|
-
const featureId = this.__annIdToFeatureId.get(annotatedId);
|
|
233
|
-
if (featureId) {
|
|
234
|
-
const url = this.makeServerUrl(annotatedId, 'annotator/')
|
|
235
|
-
const annotations = await loadJSON(url);
|
|
236
|
-
for (const annotation of annotations) { // In order of most recent to oldest
|
|
237
|
-
if ('rdfs:comment' in annotation) {
|
|
238
|
-
this.__searchIndex.indexText(featureId, annotation['rdfs:comment']);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
223
|
async setupUserInteractions_()
|
|
246
224
|
//============================
|
|
247
225
|
{
|
|
@@ -1237,20 +1215,6 @@ class FlatMap
|
|
|
1237
1215
|
|
|
1238
1216
|
//==========================================================================
|
|
1239
1217
|
|
|
1240
|
-
/**
|
|
1241
|
-
* Highlight features on the map.
|
|
1242
|
-
*
|
|
1243
|
-
* @param {Array.<string>} externalIds An array of anaotomical terms identifing features to highlight
|
|
1244
|
-
*/
|
|
1245
|
-
highlightFeatures(externalIds)
|
|
1246
|
-
//============================
|
|
1247
|
-
{
|
|
1248
|
-
if (this._userInteractions !== null) {
|
|
1249
|
-
const featureIds = this.modelFeatureIdList(externalIds);
|
|
1250
|
-
this._userInteractions.highlightFeatures(featureIds);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
1218
|
/**
|
|
1255
1219
|
* Select features on the map.
|
|
1256
1220
|
*
|
|
@@ -1435,7 +1399,6 @@ export class MapManager
|
|
|
1435
1399
|
* @arg options.showPosition {boolean} Show ``position`` of tooltip.
|
|
1436
1400
|
* @arg options.standalone {boolean} Viewer is running ``standalone``, as opposed to integrated into
|
|
1437
1401
|
* another application so show a number of controls. Defaults to ``false``.
|
|
1438
|
-
* @arg options.annotator {boolean} Allow interactive annotation of features and paths.
|
|
1439
1402
|
* @example
|
|
1440
1403
|
* const humanMap1 = mapManager.loadMap('humanV1', 'div-1');
|
|
1441
1404
|
*
|
package/src/interactions.js
CHANGED
|
@@ -33,14 +33,13 @@ import polylabel from 'polylabel';
|
|
|
33
33
|
|
|
34
34
|
//==============================================================================
|
|
35
35
|
|
|
36
|
-
import {Annotator} from './annotation';
|
|
37
36
|
import {LayerManager} from './layers';
|
|
38
37
|
import {PATHWAYS_LAYER, PathManager} from './pathways';
|
|
39
38
|
import {VECTOR_TILES_SOURCE} from './styling';
|
|
40
39
|
import {SystemsManager} from './systems';
|
|
41
40
|
|
|
42
41
|
import {displayedProperties, InfoControl} from './controls/info';
|
|
43
|
-
import {
|
|
42
|
+
import {BackgroundControl, LayerControl, NerveControl,
|
|
44
43
|
SCKANControl} from './controls/controls';
|
|
45
44
|
import {PathControl} from './controls/paths';
|
|
46
45
|
import {SearchControl} from './controls/search';
|
|
@@ -152,13 +151,6 @@ export class UserInteractions
|
|
|
152
151
|
this.enableCentrelines(this.__pathManager.enabledCentrelines, true);
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
// Add annotation capability
|
|
156
|
-
if (flatmap.options.annotator) {
|
|
157
|
-
this.__setupAnnotation();
|
|
158
|
-
} else {
|
|
159
|
-
this.__annotator = null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
154
|
// Note features that are FC systems
|
|
163
155
|
this.__systemsManager = new SystemsManager(this._flatmap, this, featuresEnabled);
|
|
164
156
|
|
|
@@ -196,11 +188,6 @@ export class UserInteractions
|
|
|
196
188
|
// Connectivity taxon control for AC maps
|
|
197
189
|
this._map.addControl(new TaxonsControl(flatmap));
|
|
198
190
|
}
|
|
199
|
-
|
|
200
|
-
if (flatmap.options.annotator) {
|
|
201
|
-
// Show/hide annotated paths
|
|
202
|
-
this._map.addControl(new AnnotatedControl(this, flatmap.options.layerOptions));
|
|
203
|
-
}
|
|
204
191
|
}
|
|
205
192
|
|
|
206
193
|
// Handle mouse events
|
|
@@ -256,41 +243,6 @@ export class UserInteractions
|
|
|
256
243
|
}
|
|
257
244
|
}
|
|
258
245
|
|
|
259
|
-
async __setupAnnotation()
|
|
260
|
-
//=======================
|
|
261
|
-
{
|
|
262
|
-
// Add annotation capability
|
|
263
|
-
|
|
264
|
-
this.__annotator = new Annotator(this._flatmap, this);
|
|
265
|
-
const annotated_features = await this.__annotator.annotated_features();
|
|
266
|
-
|
|
267
|
-
// Flag features that have annotations
|
|
268
|
-
this.__featureIdToMapId = new Map();
|
|
269
|
-
for (const [mapId, ann] of this._flatmap.annotations) {
|
|
270
|
-
this.__featureIdToMapId.set(ann.id, mapId);
|
|
271
|
-
const feature = this.mapFeature(mapId);
|
|
272
|
-
if (feature !== undefined) {
|
|
273
|
-
this._map.setFeatureState(feature, { 'map-annotation': true });
|
|
274
|
-
if (annotated_features.includes(ann.id)) {
|
|
275
|
-
this._map.setFeatureState(feature, { 'annotated': true });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
setFeatureAnnotated(featureId)
|
|
282
|
-
//============================
|
|
283
|
-
{
|
|
284
|
-
if (this.__annotator) {
|
|
285
|
-
// featureId v's geoJSON id
|
|
286
|
-
const mapId = this.__featureIdToMapId.get(featureId);
|
|
287
|
-
const feature = this.mapFeature(mapId);
|
|
288
|
-
if (feature !== undefined) {
|
|
289
|
-
this._map.setFeatureState(feature, { 'annotated': true });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
246
|
setPaint(options)
|
|
295
247
|
//===============
|
|
296
248
|
{
|
|
@@ -495,19 +447,6 @@ export class UserInteractions
|
|
|
495
447
|
}
|
|
496
448
|
}
|
|
497
449
|
|
|
498
|
-
highlightFeature_(featureId)
|
|
499
|
-
//==========================
|
|
500
|
-
{
|
|
501
|
-
featureId = +featureId; // Ensure numeric
|
|
502
|
-
this.__activateFeature(this.mapFeature(featureId));
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
unhighlightFeatures_()
|
|
506
|
-
//====================
|
|
507
|
-
{
|
|
508
|
-
this.resetActiveFeatures_();
|
|
509
|
-
}
|
|
510
|
-
|
|
511
450
|
smallestAnnotatedPolygonFeature_(features)
|
|
512
451
|
//========================================
|
|
513
452
|
{
|
|
@@ -556,30 +495,6 @@ export class UserInteractions
|
|
|
556
495
|
this.unselectFeatures();
|
|
557
496
|
}
|
|
558
497
|
|
|
559
|
-
/**
|
|
560
|
-
* Highlight features on the map.
|
|
561
|
-
*
|
|
562
|
-
* @param {Array.<string>} featureIds An array of feature identifiers to highlight
|
|
563
|
-
*/
|
|
564
|
-
highlightFeatures(featureIds)
|
|
565
|
-
//===========================
|
|
566
|
-
{
|
|
567
|
-
if (featureIds.length) {
|
|
568
|
-
this.unhighlightFeatures_();
|
|
569
|
-
for (const featureId of featureIds) {
|
|
570
|
-
const annotation = this._flatmap.annotation(featureId);
|
|
571
|
-
if (annotation) {
|
|
572
|
-
this.highlightFeature_(featureId);
|
|
573
|
-
if ('type' in annotation && annotation.type.startsWith('line')) {
|
|
574
|
-
for (const pathFeatureId of this.__pathManager.lineFeatureIds([featureId])) {
|
|
575
|
-
this.highlightFeature_(pathFeatureId);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
498
|
/**
|
|
584
499
|
* Select features on the map.
|
|
585
500
|
*
|
|
@@ -1018,28 +933,6 @@ export class UserInteractions
|
|
|
1018
933
|
}
|
|
1019
934
|
}
|
|
1020
935
|
|
|
1021
|
-
__annotationEvent(features)
|
|
1022
|
-
//=========================
|
|
1023
|
-
{
|
|
1024
|
-
if (!this.__annotator) {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
event.preventDefault();
|
|
1029
|
-
|
|
1030
|
-
// Remove any tooltip
|
|
1031
|
-
this.removeTooltip_();
|
|
1032
|
-
|
|
1033
|
-
// Don't respond to mouse events while the dialog is open
|
|
1034
|
-
this.setModal_();
|
|
1035
|
-
|
|
1036
|
-
// The annotation dialog...
|
|
1037
|
-
this.__annotator.annotate(features, () => {
|
|
1038
|
-
this.unselectFeatures();
|
|
1039
|
-
this.__clearModal();
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
936
|
clickEvent_(event)
|
|
1044
937
|
//================
|
|
1045
938
|
{
|
|
@@ -1054,14 +947,8 @@ export class UserInteractions
|
|
|
1054
947
|
this.unselectFeatures();
|
|
1055
948
|
return;
|
|
1056
949
|
}
|
|
1057
|
-
const originalEvent = event.originalEvent;
|
|
1058
|
-
if (originalEvent.altKey) {
|
|
1059
|
-
this.__annotationEvent(clickedFeatures);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
950
|
const clickedFeature = clickedFeatures[0];
|
|
1064
|
-
this.selectionEvent_(originalEvent, clickedFeature);
|
|
951
|
+
this.selectionEvent_(event.originalEvent, clickedFeature);
|
|
1065
952
|
if (this._modal) {
|
|
1066
953
|
// Remove tooltip, reset active features, etc
|
|
1067
954
|
this.__resetFeatureDisplay();
|
package/src/main.js
CHANGED
package/src/styling.js
CHANGED
|
@@ -52,7 +52,7 @@ const STROKE_INTERPOLATION = [
|
|
|
52
52
|
['zoom'],
|
|
53
53
|
2, ["*", ['var', 'width'], ["^", 2, -0.5]],
|
|
54
54
|
7, ["*", ['var', 'width'], ["^", 2, 2.5]],
|
|
55
|
-
9, ["*", ['var', 'width'], ["^", 2,
|
|
55
|
+
9, ["*", ['var', 'width'], ["^", 2, 3.0]]
|
|
56
56
|
];
|
|
57
57
|
|
|
58
58
|
//==============================================================================
|
|
@@ -162,7 +162,7 @@ export class FeatureFillLayer extends VectorStyleLayer
|
|
|
162
162
|
'fill-opacity': [
|
|
163
163
|
'case',
|
|
164
164
|
['boolean', ['feature-state', 'hidden'], false], 0.1,
|
|
165
|
-
['boolean', ['feature-state', 'selected'], false], 0.
|
|
165
|
+
['boolean', ['feature-state', 'selected'], false], 0.2,
|
|
166
166
|
['has', 'opacity'], ['get', 'opacity'],
|
|
167
167
|
['has', 'colour'], 1.0,
|
|
168
168
|
['boolean', ['feature-state', 'active'], false], 0.7,
|
|
@@ -206,53 +206,37 @@ export class FeatureBorderLayer extends VectorStyleLayer
|
|
|
206
206
|
const outlined = !('outline' in options) || options.outline;
|
|
207
207
|
const dimmed = 'dimmed' in options && options.dimmed;
|
|
208
208
|
const activeRasterLayer = 'activeRasterLayer' in options && options.activeRasterLayer;
|
|
209
|
-
const lineColour = [
|
|
210
|
-
lineColour.push(['boolean', ['feature-state', 'hidden'], false]);
|
|
211
|
-
lineColour.push(
|
|
212
|
-
lineColour.push(['boolean', ['feature-state', 'selected'], false]);
|
|
213
|
-
lineColour.push(FEATURE_SELECTED_BORDER);
|
|
209
|
+
const lineColour = ['case'];
|
|
210
|
+
lineColour.push(['boolean', ['feature-state', 'hidden'], false], COLOUR_HIDDEN);
|
|
211
|
+
lineColour.push(['boolean', ['feature-state', 'selected'], false], FEATURE_SELECTED_BORDER);
|
|
214
212
|
if (coloured && outlined) {
|
|
215
|
-
lineColour.push(['boolean', ['feature-state', 'active'], false]);
|
|
216
|
-
lineColour.push(COLOUR_ACTIVE);
|
|
213
|
+
lineColour.push(['boolean', ['feature-state', 'active'], false], COLOUR_ACTIVE);
|
|
217
214
|
}
|
|
218
|
-
lineColour.push(['boolean', ['feature-state', 'annotated'], false]);
|
|
219
|
-
lineColour.push(
|
|
220
|
-
lineColour.push(['has', 'colour']);
|
|
221
|
-
lineColour.push(['get', 'colour']);
|
|
215
|
+
lineColour.push(['boolean', ['feature-state', 'annotated'], false], COLOUR_ANNOTATED);
|
|
216
|
+
lineColour.push(['has', 'colour'], ['get', 'colour']);
|
|
222
217
|
lineColour.push('#444');
|
|
223
218
|
|
|
224
|
-
const lineOpacity = [
|
|
225
|
-
|
|
226
|
-
['boolean', ['feature-state', 'hidden'], false], 0.05,
|
|
227
|
-
];
|
|
219
|
+
const lineOpacity = ['case'];
|
|
220
|
+
lineOpacity.push(['boolean', ['feature-state', 'hidden'], false], 0.05);
|
|
228
221
|
if (coloured && outlined) {
|
|
229
|
-
lineOpacity.push(['boolean', ['feature-state', 'active'], false]);
|
|
230
|
-
lineOpacity.push(0.9);
|
|
222
|
+
lineOpacity.push(['boolean', ['feature-state', 'active'], false], 0.9);
|
|
231
223
|
}
|
|
232
|
-
lineOpacity.push(['boolean', ['feature-state', 'selected'], false]);
|
|
233
|
-
lineOpacity.push(0.9);
|
|
234
|
-
lineOpacity.push(['boolean', ['feature-state', 'annotated'], false]);
|
|
235
|
-
lineOpacity.push(0.9);
|
|
224
|
+
lineOpacity.push(['boolean', ['feature-state', 'selected'], false], 0.9);
|
|
225
|
+
lineOpacity.push(['boolean', ['feature-state', 'annotated'], false], 0.9);
|
|
236
226
|
if (activeRasterLayer) {
|
|
237
227
|
lineOpacity.push((outlined && !dimmed) ? 0.3 : 0.1);
|
|
238
228
|
} else {
|
|
239
229
|
lineOpacity.push(0.5);
|
|
240
230
|
}
|
|
241
231
|
|
|
242
|
-
const lineWidth = [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
];
|
|
246
|
-
lineWidth.push(['boolean', ['feature-state', 'selected'], false]);
|
|
247
|
-
lineWidth.push(2.5);
|
|
232
|
+
const lineWidth = ['case'];
|
|
233
|
+
lineWidth.push(['boolean', ['get', 'invisible'], false], 0.2);
|
|
234
|
+
lineWidth.push(['boolean', ['feature-state', 'selected'], false], 1.5);
|
|
248
235
|
if (coloured && outlined) {
|
|
249
|
-
lineWidth.push(['boolean', ['feature-state', 'active'], false]);
|
|
250
|
-
lineWidth.push(1.5);
|
|
236
|
+
lineWidth.push(['boolean', ['feature-state', 'active'], false], 1.5);
|
|
251
237
|
}
|
|
252
|
-
lineWidth.push(['boolean', ['feature-state', 'annotated'], false]);
|
|
253
|
-
lineWidth.push(
|
|
254
|
-
lineWidth.push(['has', 'colour']);
|
|
255
|
-
lineWidth.push(0.7);
|
|
238
|
+
lineWidth.push(['boolean', ['feature-state', 'annotated'], false], 3.5);
|
|
239
|
+
lineWidth.push(['has', 'colour'], 0.7);
|
|
256
240
|
lineWidth.push((coloured && outlined) ? 0.5 : 0.1);
|
|
257
241
|
|
|
258
242
|
return super.changedPaintStyle({
|
|
@@ -293,14 +277,14 @@ export class FeatureLineLayer extends VectorStyleLayer
|
|
|
293
277
|
return this.__dashed ? [
|
|
294
278
|
'all',
|
|
295
279
|
['==', '$type', 'LineString'],
|
|
296
|
-
['==', 'type',
|
|
280
|
+
['==', 'type', 'line-dash']
|
|
297
281
|
] : [
|
|
298
282
|
'all',
|
|
299
283
|
['==', '$type', 'LineString'],
|
|
300
284
|
[
|
|
301
285
|
'any',
|
|
302
286
|
['==', 'type', 'bezier'],
|
|
303
|
-
['==', 'type',
|
|
287
|
+
['==', 'type', 'line']
|
|
304
288
|
]
|
|
305
289
|
];
|
|
306
290
|
}
|
|
@@ -328,12 +312,17 @@ export class FeatureLineLayer extends VectorStyleLayer
|
|
|
328
312
|
],
|
|
329
313
|
'line-width': [
|
|
330
314
|
'let',
|
|
331
|
-
'width',
|
|
332
|
-
'
|
|
333
|
-
['
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
315
|
+
'width', [
|
|
316
|
+
'*',
|
|
317
|
+
['case',
|
|
318
|
+
['has', 'stroke-width'], ['get', 'stroke-width'],
|
|
319
|
+
1.0
|
|
320
|
+
],
|
|
321
|
+
['case',
|
|
322
|
+
['boolean', ['feature-state', 'selected'], false], 1.2,
|
|
323
|
+
['boolean', ['feature-state', 'active'], false], 1.2,
|
|
324
|
+
options.authoring ? 0.7 : 0.5
|
|
325
|
+
]
|
|
337
326
|
],
|
|
338
327
|
STROKE_INTERPOLATION
|
|
339
328
|
]
|
|
@@ -526,10 +515,6 @@ export class PathLineLayer extends VectorStyleLayer
|
|
|
526
515
|
'line-color': [
|
|
527
516
|
'let', 'active', ['to-number', ['feature-state', 'active'], 0],
|
|
528
517
|
[ 'case',
|
|
529
|
-
['all',
|
|
530
|
-
['==', ['var', 'active'], 0],
|
|
531
|
-
['boolean', ['feature-state', 'selected'], false],
|
|
532
|
-
], COLOUR_SELECTED,
|
|
533
518
|
['boolean', ['feature-state', 'hidden'], false], COLOUR_HIDDEN,
|
|
534
519
|
['==', ['get', 'type'], 'bezier'], 'red',
|
|
535
520
|
...PATH_STYLE_RULES,
|
|
@@ -559,7 +544,9 @@ export class PathLineLayer extends VectorStyleLayer
|
|
|
559
544
|
"*",
|
|
560
545
|
this.__highlight ? ['case',
|
|
561
546
|
['boolean', ['get', 'invisible'], false], 0.1,
|
|
562
|
-
['boolean', ['feature-state', 'selected'], false],
|
|
547
|
+
['boolean', ['feature-state', 'selected'], false], [
|
|
548
|
+
'case', ['boolean', ['feature-state', 'active'], false], 1.2,
|
|
549
|
+
0.9],
|
|
563
550
|
['boolean', ['feature-state', 'active'], false], 0.9,
|
|
564
551
|
0.0
|
|
565
552
|
] : [
|
package/src/annotation.js
DELETED
|
@@ -1,665 +0,0 @@
|
|
|
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
|
-
'use strict';
|
|
22
|
-
|
|
23
|
-
//==============================================================================
|
|
24
|
-
|
|
25
|
-
// We use Font Awesome icons
|
|
26
|
-
import '@fortawesome/fontawesome-free/css/all.css';
|
|
27
|
-
import escape from 'html-es6cape';
|
|
28
|
-
import { jsPanel } from 'jspanel4';
|
|
29
|
-
import 'jspanel4/dist/jspanel.css';
|
|
30
|
-
|
|
31
|
-
//==============================================================================
|
|
32
|
-
|
|
33
|
-
const FETCH_TIMEOUT = 3000; // 3 seconds
|
|
34
|
-
const UPDATE_TIMEOUT = 3000; // 5 seconds
|
|
35
|
-
const LOGIN_TIMEOUT = 30000; // 30 seconds
|
|
36
|
-
const LOGOUT_TIMEOUT = 3000; // 5 seconds
|
|
37
|
-
|
|
38
|
-
const STATUS_MESSAGE_TIMEOUT = 3000;
|
|
39
|
-
|
|
40
|
-
//==============================================================================
|
|
41
|
-
|
|
42
|
-
const FEATURE_DISPLAY_PROPERTIES = {
|
|
43
|
-
'id': 'Feature',
|
|
44
|
-
'label': 'Tooltip',
|
|
45
|
-
'models': 'Models',
|
|
46
|
-
'name': 'Name',
|
|
47
|
-
'sckan': 'SCKAN valid',
|
|
48
|
-
'fc-class': 'FC class',
|
|
49
|
-
'fc-kind': 'FC kind',
|
|
50
|
-
'layer': 'Map layer',
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const ANNOTATION_FIELDS = [
|
|
54
|
-
{
|
|
55
|
-
prompt: 'Feature derived from',
|
|
56
|
-
key: 'prov:wasDerivedFrom',
|
|
57
|
-
update: true,
|
|
58
|
-
kind: 'list',
|
|
59
|
-
size: 6
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
prompt: 'Comment',
|
|
63
|
-
key: 'rdfs:comment',
|
|
64
|
-
update: false,
|
|
65
|
-
kind: 'textbox'
|
|
66
|
-
},
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
//==============================================================================
|
|
70
|
-
|
|
71
|
-
function startSpinner(panel)
|
|
72
|
-
{
|
|
73
|
-
panel.headerlogo.innerHTML = '<span class="fa fa-spinner fa-spin ml-2"></span>';
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function stopSpinner(panel)
|
|
77
|
-
{
|
|
78
|
-
panel.headerlogo.innerHTML = '';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
//==============================================================================
|
|
82
|
-
|
|
83
|
-
export class Annotator
|
|
84
|
-
{
|
|
85
|
-
constructor(flatmap, ui)
|
|
86
|
-
{
|
|
87
|
-
this.__flatmap = flatmap;
|
|
88
|
-
this.__ui = ui;
|
|
89
|
-
this.__haveAnnotation = false;
|
|
90
|
-
this.__user = undefined;
|
|
91
|
-
this.__savedStatusMessage = '';
|
|
92
|
-
this.__authorised = false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get user()
|
|
96
|
-
{
|
|
97
|
-
return this.__user;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
__creatorName(creator)
|
|
101
|
-
{
|
|
102
|
-
return creator.name || creator.email || creator.login || creator.company || creator;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
__setUser(creator)
|
|
106
|
-
{
|
|
107
|
-
this.__user = creator;
|
|
108
|
-
this.__setStatusMessage(`Annotating as ${this.__creatorName(creator)}`, 0)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
__clearUser()
|
|
112
|
-
{
|
|
113
|
-
this.__user = undefined;
|
|
114
|
-
this.__setStatusMessage('', 0);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async __authorise(panel)
|
|
118
|
-
//======================
|
|
119
|
-
{
|
|
120
|
-
const abortController = new AbortController();
|
|
121
|
-
setTimeout((panel) => {
|
|
122
|
-
if (this.user === 'undefined') {
|
|
123
|
-
console.log("Aborting login...");
|
|
124
|
-
abortController.abort();
|
|
125
|
-
stopSpinner(panel);
|
|
126
|
-
this.__setStatusMessage('Unable to login...');
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
LOGIN_TIMEOUT, panel);
|
|
130
|
-
|
|
131
|
-
const url = `${this.__flatmap._baseUrl}login`;
|
|
132
|
-
startSpinner(panel);
|
|
133
|
-
const response = await fetch(url, {
|
|
134
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
135
|
-
signal: abortController.signal
|
|
136
|
-
});
|
|
137
|
-
stopSpinner(panel);
|
|
138
|
-
if (response.ok) {
|
|
139
|
-
const user_data = await response.json();
|
|
140
|
-
if ('error' in user_data) {
|
|
141
|
-
return Promise.resolve({error: response.error});
|
|
142
|
-
} else {
|
|
143
|
-
this.__setUser(user_data);
|
|
144
|
-
this.__authorised = true;
|
|
145
|
-
return Promise.resolve(user_data);
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
return Promise.resolve({error: `${response.status} ${response.statusText}`});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async __unauthorise()
|
|
153
|
-
//===================
|
|
154
|
-
{
|
|
155
|
-
const abortController = new AbortController();
|
|
156
|
-
setTimeout(() => {
|
|
157
|
-
if (this.__authorised) {
|
|
158
|
-
console.log("Aborting logout...");
|
|
159
|
-
abortController.abort();
|
|
160
|
-
this.__setStatusMessage('Unable to logout...');
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
LOGOUT_TIMEOUT);
|
|
164
|
-
|
|
165
|
-
const url = `${this.__flatmap._baseUrl}logout`;
|
|
166
|
-
const response = fetch(url, {
|
|
167
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
168
|
-
signal: abortController.signal
|
|
169
|
-
});
|
|
170
|
-
if (response.ok) {
|
|
171
|
-
this.__authorised = false;
|
|
172
|
-
return response.json();
|
|
173
|
-
} else {
|
|
174
|
-
return Promise.resolve({error: `${response.status} ${response.statusText}`});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
__setStatusMessage(message, timeout=STATUS_MESSAGE_TIMEOUT)
|
|
179
|
-
//=========================================================
|
|
180
|
-
{
|
|
181
|
-
if (timeout == 0) {
|
|
182
|
-
this.__savedStatusMessage = message;
|
|
183
|
-
}
|
|
184
|
-
this.__statusMessage.innerHTML = message;
|
|
185
|
-
if (+timeout > 0) {
|
|
186
|
-
setTimeout(() => {
|
|
187
|
-
this.__statusMessage.innerHTML = this.__savedStatusMessage;
|
|
188
|
-
}, +timeout);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
__featureHtml(featureProperties)
|
|
193
|
-
//==============================
|
|
194
|
-
{
|
|
195
|
-
// Feature properties
|
|
196
|
-
const html = [];
|
|
197
|
-
for (const [key, prompt] of Object.entries(FEATURE_DISPLAY_PROPERTIES)) {
|
|
198
|
-
const value = featureProperties[key];
|
|
199
|
-
if (value !== undefined && value !== '') {
|
|
200
|
-
const escapedValue = escape(value).replaceAll('\n', '<br/>');
|
|
201
|
-
html.push(`<div><span class="flatmap-annotation-prompt">${prompt}:</span><span class="flatmap-annotation-value">${escapedValue}</span></div>`)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return html;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
__annotationHtml(annotations)
|
|
208
|
-
//===========================
|
|
209
|
-
{
|
|
210
|
-
const html = [];
|
|
211
|
-
let firstBlock = true;
|
|
212
|
-
for (const annotation of annotations) {
|
|
213
|
-
if (firstBlock) {
|
|
214
|
-
firstBlock = false;
|
|
215
|
-
} else {
|
|
216
|
-
html.push('<hr/>')
|
|
217
|
-
}
|
|
218
|
-
if (annotation['rdf:type'] === 'prov:Entity') {
|
|
219
|
-
const annotator = this.__creatorName(annotation['dct:creator']);
|
|
220
|
-
html.push(`<div><span class="flatmap-annotation-prompt">${annotation['dct:created']}</span><span class="flatmap-annotation-value">${annotator}</span></div>`);
|
|
221
|
-
for (const field of ANNOTATION_FIELDS) {
|
|
222
|
-
const value = annotation[field.key];
|
|
223
|
-
if (value !== undefined && value !== '') {
|
|
224
|
-
const escapedValue = (field.kind === 'list')
|
|
225
|
-
? value.filter(v => v.trim()).map(v => escape(v.trim())).join(', ')
|
|
226
|
-
: escape(value).replaceAll('\n', '<br/>');
|
|
227
|
-
html.push(`<div><span class="flatmap-annotation-prompt">${field.prompt}:</span><span class="flatmap-annotation-value">${escapedValue}</span></div>`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return html.join('\n');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
__editFormHtml(provenanceData)
|
|
236
|
-
//============================
|
|
237
|
-
{
|
|
238
|
-
const html = [];
|
|
239
|
-
html.push('<div id="flatmap-annotation-formdata">');
|
|
240
|
-
for (const field of ANNOTATION_FIELDS) {
|
|
241
|
-
html.push('<div class="flatmap-annotation-entry">');
|
|
242
|
-
html.push(` <label for="${field.key}">${field.prompt}:</label>`);
|
|
243
|
-
if (field.kind === 'textbox') {
|
|
244
|
-
const value = field.update ? provenanceData[field.key] || '' : '';
|
|
245
|
-
html.push(` <textarea rows="5" cols="40" id="${field.key}" name="${field.key}">${value.trim()}</textarea>`)
|
|
246
|
-
} else if (!('kind' in field) || field.kind !== 'list') {
|
|
247
|
-
const value = field.update ? provenanceData[field.key] || '' : '';
|
|
248
|
-
html.push(` <input type="text" size="40" id="${field.key}" name="${field.key}" value="${value.trim()}"/>`)
|
|
249
|
-
} else { // field.kind === 'list'
|
|
250
|
-
const listValues = field.update ? provenanceData[field.key] || [] : [];
|
|
251
|
-
html.push(' <div class="multiple">')
|
|
252
|
-
for (let n = 1; n <= field.size; n++) {
|
|
253
|
-
const fieldValue = (n <= listValues.length) ? listValues[n-1].trim() : '';
|
|
254
|
-
html.push(` <input type="text" size="40" id="${field.key}_${n}" name="${field.key}" value="${fieldValue}"/>`)
|
|
255
|
-
}
|
|
256
|
-
html.push(' </div>')
|
|
257
|
-
}
|
|
258
|
-
html.push('</div>');
|
|
259
|
-
}
|
|
260
|
-
html.push(' <div><input id="annotation-save-button" type="button" value="Save"/></div>');
|
|
261
|
-
html.push('</div>');
|
|
262
|
-
return html.join('\n');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
__provenanceData(annotations)
|
|
266
|
-
//===========================
|
|
267
|
-
{
|
|
268
|
-
const provenanceData = {};
|
|
269
|
-
for (const annotation of annotations) { // In order of most recent to oldest
|
|
270
|
-
if (annotation['rdf:type'] === 'prov:Entity') {
|
|
271
|
-
for (const field of ANNOTATION_FIELDS) {
|
|
272
|
-
if (field.update) {
|
|
273
|
-
const value = annotation[field.key];
|
|
274
|
-
if (value !== undefined && !(field.key in provenanceData)) {
|
|
275
|
-
provenanceData[field.key] = value;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return provenanceData;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
__changedAnnotation(provenanceData)
|
|
285
|
-
//=================================
|
|
286
|
-
{
|
|
287
|
-
const newProperties = {};
|
|
288
|
-
let propertiesChanged = false;
|
|
289
|
-
for (const field of ANNOTATION_FIELDS) {
|
|
290
|
-
if (!('kind' in field) || field.kind !== 'list') {
|
|
291
|
-
const lastValue = field.update ? provenanceData[field.key] || '' : '';
|
|
292
|
-
const inputField = document.getElementById(field.key);
|
|
293
|
-
const newValue = inputField.value.trim();
|
|
294
|
-
if (newValue !== lastValue.trim()) {
|
|
295
|
-
newProperties[field.key] = newValue;
|
|
296
|
-
propertiesChanged = true;
|
|
297
|
-
}
|
|
298
|
-
} else { // field.kind === 'list'
|
|
299
|
-
const listValues = [];
|
|
300
|
-
for (let n = 1; n <= field.size; n++) {
|
|
301
|
-
const inputField = document.getElementById(`${field.key}_${n}`);
|
|
302
|
-
listValues.push(inputField.value.trim());
|
|
303
|
-
}
|
|
304
|
-
const lastValue = field.update ? provenanceData[field.key] || [] : [];
|
|
305
|
-
const oldValues = lastValue.map(v => v.trim()).filter(v => (v !== '')).sort(Intl.Collator().compare);
|
|
306
|
-
const newValues = listValues.map(v => v.trim()).filter(v => (v !== '')).sort(Intl.Collator().compare);
|
|
307
|
-
if (oldValues.length !== newValues.length
|
|
308
|
-
|| oldValues.filter(v => !newValues.includes(v)).length > 0) {
|
|
309
|
-
newProperties[field.key] = newValues;
|
|
310
|
-
propertiesChanged = true;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
changed: propertiesChanged,
|
|
316
|
-
properties: newProperties
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async __updateRemoteAnnotation(panel, annotation)
|
|
321
|
-
//===============================================
|
|
322
|
-
{
|
|
323
|
-
const abortController = new AbortController();
|
|
324
|
-
|
|
325
|
-
setTimeout((panel) => {
|
|
326
|
-
if (panel.status !== 'closed') {
|
|
327
|
-
console.log("Aborting remote update...");
|
|
328
|
-
abortController.abort();
|
|
329
|
-
stopSpinner(panel);
|
|
330
|
-
this.__setStatusMessage('Cannot update annotation...');
|
|
331
|
-
}
|
|
332
|
-
}, UPDATE_TIMEOUT, panel);
|
|
333
|
-
|
|
334
|
-
const url = this.__flatmap.makeServerUrl(this.__currentFeatureId, 'annotator/');
|
|
335
|
-
const response = await fetch(url, {
|
|
336
|
-
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
337
|
-
method: 'POST',
|
|
338
|
-
body: JSON.stringify(annotation),
|
|
339
|
-
signal: abortController.signal
|
|
340
|
-
});
|
|
341
|
-
if (response.ok) {
|
|
342
|
-
return response.json();
|
|
343
|
-
} else {
|
|
344
|
-
return Promise.resolve({error: `${response.status} ${response.statusText}`});
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async __saveAnnotation(panel, provenanceData)
|
|
349
|
-
//===========================================
|
|
350
|
-
{
|
|
351
|
-
const changedProperties = this.__changedAnnotation(provenanceData);
|
|
352
|
-
if (this.__currentFeatureId !== undefined && changedProperties.changed) {
|
|
353
|
-
const annotation = {
|
|
354
|
-
...changedProperties.properties,
|
|
355
|
-
'rdf:type': 'prov:Entity',
|
|
356
|
-
'dct:subject': `flatmaps:${this.__flatmap.uuid}/${this.__currentFeatureId}`,
|
|
357
|
-
'dct:creator': this.user
|
|
358
|
-
}
|
|
359
|
-
startSpinner(panel);
|
|
360
|
-
const response = await this.__updateRemoteAnnotation(panel, annotation);
|
|
361
|
-
stopSpinner(panel);
|
|
362
|
-
if ('error' in response) {
|
|
363
|
-
this.__setStatusMessage(response.error);
|
|
364
|
-
} else {
|
|
365
|
-
this.__flatmap.setFeatureAnnotated(this.__currentFeatureId);
|
|
366
|
-
panel.close();
|
|
367
|
-
}
|
|
368
|
-
} else {
|
|
369
|
-
this.__setStatusMessage('No changes to save...');
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
__finishPanelContent(panel, response)
|
|
374
|
-
//====================================
|
|
375
|
-
{
|
|
376
|
-
this.__haveAnnotation = true;
|
|
377
|
-
const provenanceData = this.__provenanceData(response);
|
|
378
|
-
this.__existingAnnotation.innerHTML = this.__annotationHtml(response);
|
|
379
|
-
this.__annotationForm.innerHTML = this.__editFormHtml(provenanceData);
|
|
380
|
-
|
|
381
|
-
// Lock focus to focusable elements within the panel
|
|
382
|
-
const inputElements = panel.content.querySelectorAll('input, textarea, button');
|
|
383
|
-
this.__firstInputField = inputElements[0];
|
|
384
|
-
const lastInput = inputElements[inputElements.length - 1];
|
|
385
|
-
const saveButton = document.getElementById('annotation-save-button');
|
|
386
|
-
|
|
387
|
-
panel.addEventListener('keydown', function (e) {
|
|
388
|
-
if (e.key === 'Tab') {
|
|
389
|
-
if ( e.shiftKey ) /* shift + tab */ {
|
|
390
|
-
if (document.activeElement === this.__firstInputField) {
|
|
391
|
-
lastInput.focus();
|
|
392
|
-
e.preventDefault();
|
|
393
|
-
}
|
|
394
|
-
} else /* tab */ {
|
|
395
|
-
if (document.activeElement === lastInput) {
|
|
396
|
-
this.__firstInputField.focus();
|
|
397
|
-
e.preventDefault();
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
} else if (e.key === 'Enter') {
|
|
401
|
-
if (e.target === saveButton) {
|
|
402
|
-
this.__saveAnnotation(panel, provenanceData);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}.bind(this));
|
|
406
|
-
|
|
407
|
-
saveButton.addEventListener('mousedown', function (e) {
|
|
408
|
-
this.__saveAnnotation(panel, provenanceData);
|
|
409
|
-
}.bind(this));
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
__panelCallback(panel)
|
|
413
|
-
//====================
|
|
414
|
-
{
|
|
415
|
-
this.__annotationForm = document.getElementById('flatmap-annotation-form');
|
|
416
|
-
// Data entry only once authorised
|
|
417
|
-
this.__annotationForm.hidden = true;
|
|
418
|
-
|
|
419
|
-
// Populate once we have content from server
|
|
420
|
-
this.__existingAnnotation = document.getElementById('flatmap-annotation-existing');
|
|
421
|
-
this.__statusMessage = document.getElementById('flatmap-annotation-status');
|
|
422
|
-
|
|
423
|
-
this.__authoriseLock = document.getElementById('flatmap-annotation-lock');
|
|
424
|
-
this.__authoriseLock.addEventListener('click', (e) => {
|
|
425
|
-
const lockClasses = this.__authoriseLock.classList;
|
|
426
|
-
if (lockClasses.contains('fa-lock')) {
|
|
427
|
-
this.__authorise(panel).then((response) => {
|
|
428
|
-
if ('error' in response) {
|
|
429
|
-
this.__setStatusMessage(response.error);
|
|
430
|
-
} else {
|
|
431
|
-
this.__annotationForm.hidden = false;
|
|
432
|
-
this.__firstInputField.focus();
|
|
433
|
-
lockClasses.remove('fa-lock');
|
|
434
|
-
lockClasses.add('fa-unlock');
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
} else {
|
|
438
|
-
this.__unauthorise().then((response) => {
|
|
439
|
-
console.log(`Annotator logout: ${response}`);
|
|
440
|
-
});
|
|
441
|
-
this.__annotationForm.hidden = true;
|
|
442
|
-
lockClasses.remove('fa-unlock');
|
|
443
|
-
lockClasses.add('fa-lock');
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
__chooseFeatureProperties(features, callback)
|
|
449
|
-
//===========================================
|
|
450
|
-
{
|
|
451
|
-
this.__ui.selectFeature(features[0].id);
|
|
452
|
-
|
|
453
|
-
// Feature chooser is only for multiple selections
|
|
454
|
-
if (features.length === 1
|
|
455
|
-
|| features[0].properties['cd-class'] !== 'celldl:Connection'
|
|
456
|
-
|| (features.length === 2
|
|
457
|
-
&& features[1].properties['cd-class'] !== 'celldl:Connection')) {
|
|
458
|
-
callback(features[0].properties);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const featureList = [];
|
|
462
|
-
const featureProperties = new Map();
|
|
463
|
-
const featureSeen = new Set();
|
|
464
|
-
let selected = 'selected'; // Select the first entry
|
|
465
|
-
for (const feature of features) {
|
|
466
|
-
if (feature.properties['cd-class'] !== 'celldl:Connection'
|
|
467
|
-
|| feature.properties['id'] == undefined
|
|
468
|
-
|| featureSeen.has(feature.properties['id'])) {
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
const mapFeature = this.__ui.mapFeature(feature.id);
|
|
472
|
-
const annotated = (mapFeature !== undefined)
|
|
473
|
-
? this.__ui._map.getFeatureState(mapFeature)['annotated']
|
|
474
|
-
: false;
|
|
475
|
-
let label = '';
|
|
476
|
-
if (feature.properties.models) {
|
|
477
|
-
label = ` -- ${feature.properties.label.split('\n')[0]} (${feature.properties.models})`;
|
|
478
|
-
} else if (feature.properties.label) {
|
|
479
|
-
label = ` -- ${feature.properties.label.split('\n')[0]}`;
|
|
480
|
-
}
|
|
481
|
-
featureList.push(`<option value="${feature.id}" ${selected}>${annotated ? '*' : ' '} ${feature.properties.id} -- ${feature.properties.kind}${label}</option>`);
|
|
482
|
-
featureProperties.set(+feature.id, feature.properties);
|
|
483
|
-
featureSeen.add(feature.properties['id']);
|
|
484
|
-
selected = '';
|
|
485
|
-
}
|
|
486
|
-
if (featureList.length == 0) {
|
|
487
|
-
callback(undefined);
|
|
488
|
-
return;
|
|
489
|
-
} else if (featureList.length == 1) {
|
|
490
|
-
callback(featureProperties.values().next().value);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const panelContent = `
|
|
494
|
-
<div id="annotation-feature-selection">
|
|
495
|
-
<div>
|
|
496
|
-
<label for="annotation-feature-selector">Select feature:</label>
|
|
497
|
-
<select id="annotation-feature-selector" size="${Math.min(featureList.length, 7)}">
|
|
498
|
-
${featureList.join('\n')}
|
|
499
|
-
</select>
|
|
500
|
-
</div>
|
|
501
|
-
<div id="annotation-feature-buttons">
|
|
502
|
-
<input id="annotation-feature-cancel" type="button" value="Cancel"/>
|
|
503
|
-
<input id="annotation-feature-annotate" type="button" value="Annotate"/>
|
|
504
|
-
</div>
|
|
505
|
-
</div>`;
|
|
506
|
-
this.__panel = jsPanel.create({
|
|
507
|
-
theme: 'light',
|
|
508
|
-
border: '2px solid #080',
|
|
509
|
-
borderRadius: '.5rem',
|
|
510
|
-
panelSize: 'auto auto',
|
|
511
|
-
position: 'left-top 50 70',
|
|
512
|
-
content: panelContent,
|
|
513
|
-
data: features[0].properties,
|
|
514
|
-
closeOnEscape: true,
|
|
515
|
-
closeOnBackdrop: false,
|
|
516
|
-
headerTitle: 'Select feature to annotate',
|
|
517
|
-
headerControls: 'closeonly xs',
|
|
518
|
-
callback: ((panel) => {
|
|
519
|
-
const selector = document.getElementById('annotation-feature-selector');
|
|
520
|
-
selector.onchange = (e) => {
|
|
521
|
-
if (e.target.value !== '') {
|
|
522
|
-
this.__ui.unselectFeatures();
|
|
523
|
-
this.__ui.selectFeature(e.target.value);
|
|
524
|
-
this.__panel.options.data = featureProperties.get(+e.target.value);
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
selector.ondblclick = (e) => {
|
|
528
|
-
if (e.target.value !== '') {
|
|
529
|
-
const properties = this.__panel.options.data;
|
|
530
|
-
this.__panel.close();
|
|
531
|
-
callback(properties);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
selector.focus();
|
|
535
|
-
document.getElementById('annotation-feature-cancel')
|
|
536
|
-
.onclick = (e) => {
|
|
537
|
-
this.__panel.close();
|
|
538
|
-
callback(undefined);
|
|
539
|
-
};
|
|
540
|
-
document.getElementById('annotation-feature-annotate')
|
|
541
|
-
.onclick = (e) => {
|
|
542
|
-
const properties = this.__panel.options.data;
|
|
543
|
-
this.__panel.close();
|
|
544
|
-
callback(properties);
|
|
545
|
-
};
|
|
546
|
-
}).bind(this)
|
|
547
|
-
});
|
|
548
|
-
document.addEventListener('jspanelcloseduser', (e) => { callback(undefined) }, false);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
annotate(features, closedCallback)
|
|
552
|
-
//================================
|
|
553
|
-
{
|
|
554
|
-
// provide a list of features so dialog needs to first provide selection list
|
|
555
|
-
// and highlight current one as user scrolls...
|
|
556
|
-
|
|
557
|
-
this.__chooseFeatureProperties(features, (featureProperties) => {
|
|
558
|
-
if (featureProperties) {
|
|
559
|
-
this.__annotateFeature(featureProperties, closedCallback);
|
|
560
|
-
} else {
|
|
561
|
-
closedCallback();
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
__annotateFeature(featureProperties, callback)
|
|
567
|
-
//============================================
|
|
568
|
-
{
|
|
569
|
-
this.__currentFeatureId = featureProperties['id'];
|
|
570
|
-
if (this.__currentFeatureId === undefined) {
|
|
571
|
-
callback();
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
const panelContent = [];
|
|
575
|
-
panelContent.push('<div id="flatmap-annotation-panel">');
|
|
576
|
-
panelContent.push(' <div id="flatmap-annotation-feature">');
|
|
577
|
-
panelContent.push(...this.__featureHtml(featureProperties));
|
|
578
|
-
panelContent.push(' </div>');
|
|
579
|
-
panelContent.push(' <form id="flatmap-annotation-form"></form>');
|
|
580
|
-
panelContent.push(' <div id="flatmap-annotation-existing"></div>');
|
|
581
|
-
panelContent.push('</div>');
|
|
582
|
-
|
|
583
|
-
const annotator = this; // To use in panel creation code
|
|
584
|
-
const flatmap = this.__flatmap; // To use in panel creation code
|
|
585
|
-
const contentFetchAbort = new AbortController();
|
|
586
|
-
this.__panel = jsPanel.create({
|
|
587
|
-
theme: 'light',
|
|
588
|
-
border: '2px solid #080',
|
|
589
|
-
borderRadius: '.5rem',
|
|
590
|
-
panelSize: '725px auto',
|
|
591
|
-
position: 'left-top 50 70',
|
|
592
|
-
data: {
|
|
593
|
-
flatmap: this.__flatmap
|
|
594
|
-
},
|
|
595
|
-
content: panelContent.join('\n'),
|
|
596
|
-
closeOnEscape: true,
|
|
597
|
-
closeOnBackdrop: false,
|
|
598
|
-
headerTitle: 'Feature annotations',
|
|
599
|
-
headerControls: 'closeonly xs',
|
|
600
|
-
footerToolbar: [
|
|
601
|
-
'<span id="flatmap-annotation-status" class="flex-auto"></span>',
|
|
602
|
-
'<span id="flatmap-annotation-lock" class="jsPanel-ftr-btn fa fa-lock"></span>',
|
|
603
|
-
],
|
|
604
|
-
contentFetch: {
|
|
605
|
-
resource: flatmap.makeServerUrl(this.__currentFeatureId, 'annotator/'),
|
|
606
|
-
fetchInit: {
|
|
607
|
-
method: 'GET',
|
|
608
|
-
mode: 'cors',
|
|
609
|
-
headers: {
|
|
610
|
-
"Accept": "application/json; charset=utf-8",
|
|
611
|
-
"Cache-Control": "no-store"
|
|
612
|
-
},
|
|
613
|
-
signal: contentFetchAbort.signal
|
|
614
|
-
},
|
|
615
|
-
bodyMethod: 'json',
|
|
616
|
-
beforeSend: (fetchConfig, panel) => {
|
|
617
|
-
startSpinner(panel);
|
|
618
|
-
setTimeout((panel) => {
|
|
619
|
-
if (!annotator.__haveAnnotation) {
|
|
620
|
-
console.log("Aborting content fetch...");
|
|
621
|
-
contentFetchAbort.abort();
|
|
622
|
-
stopSpinner(panel);
|
|
623
|
-
annotator.__setStatusMessage('Cannot fetch annotation...');
|
|
624
|
-
annotator.__authoriseLock.className = '';
|
|
625
|
-
}
|
|
626
|
-
}, FETCH_TIMEOUT, panel);
|
|
627
|
-
},
|
|
628
|
-
done: (response, panel) => {
|
|
629
|
-
annotator.__finishPanelContent(panel, response);
|
|
630
|
-
stopSpinner(panel);
|
|
631
|
-
}
|
|
632
|
-
},
|
|
633
|
-
callback: annotator.__panelCallback.bind(annotator)
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
// should we warn if unsaved changes when closing??
|
|
637
|
-
document.addEventListener('jspanelclosed', callback, false);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
async annotated_features()
|
|
641
|
-
//========================
|
|
642
|
-
{
|
|
643
|
-
const url = this.__flatmap.makeServerUrl('', 'annotator/');
|
|
644
|
-
try {
|
|
645
|
-
const response = await fetch(url, {
|
|
646
|
-
headers: {
|
|
647
|
-
"Accept": "application/json; charset=utf-8",
|
|
648
|
-
"Cache-Control": "no-store"
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
if (response.ok) {
|
|
652
|
-
return response.json();
|
|
653
|
-
} else {
|
|
654
|
-
console.error(`Annotated features: ${response.status} ${response.statusText}`);
|
|
655
|
-
return Promise.resolve([]);
|
|
656
|
-
}
|
|
657
|
-
} catch {
|
|
658
|
-
console.error(`Fetch failed -- is annotator available at ${this.__flatmap._baseUrl} ?`);
|
|
659
|
-
return Promise.resolve([]);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
//==============================================================================
|