@abi-software/flatmap-viewer 2.4.2-b.5 → 2.4.4
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 +6 -6
- package/src/controls/controls.js +0 -67
- package/src/flatmap-viewer.js +0 -37
- package/src/interactions.js +40 -132
- package/src/main.js +0 -1
- package/src/mathjax.js +100 -0
- package/src/styling.js +35 -48
- package/src/annotation.js +0 -665
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
|
-
//==============================================================================
|