tsviewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/babel.config.js +5 -0
- package/dist/demo.html +1 -0
- package/dist/tsviewer.common.js +33247 -0
- package/dist/tsviewer.common.js.map +1 -0
- package/dist/tsviewer.umd.js +33259 -0
- package/dist/tsviewer.umd.js.map +1 -0
- package/dist/tsviewer.umd.min.js +4 -0
- package/dist/tsviewer.umd.min.js.map +1 -0
- package/jsconfig.json +19 -0
- package/package.json +61 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +17 -0
- package/src/App.vue +41 -0
- package/src/assets/icons/blackfynn-amplitude-zoom.js +11 -0
- package/src/assets/icons/blackfynn-timescale.js +11 -0
- package/src/assets/icons/icon-controller-pause.js +11 -0
- package/src/assets/icons/icon-controller-play.js +11 -0
- package/src/assets/icons/icon-next-page.js +11 -0
- package/src/assets/icons/icon-previous-page.js +11 -0
- package/src/assets/icons/icon-stopwatch.js +11 -0
- package/src/assets/icons/index.js +7 -0
- package/src/components/TSPlotCanvas.vue +1868 -0
- package/src/components/TSScrubber.vue +420 -0
- package/src/components/TSViewer.vue +595 -0
- package/src/components/TSViewerCanvas.vue +793 -0
- package/src/components/TSViewerToolbar.vue +356 -0
- package/src/components/index.js +13 -0
- package/src/main.js +23 -0
- package/src/mixins/request/index.js +100 -0
- package/src/mixins/ts-annotation/index.js +202 -0
- package/src/mixins/viewer-active-tool/index.js +36 -0
- package/src/store/index.js +184 -0
- package/src/utils/constants.js +15 -0
- package/vue.config.js +12 -0
|
@@ -0,0 +1,1868 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="timeseries-plot-canvas">
|
|
3
|
+
<canvas id="blurArea" class="canvas" ref="blurArea"
|
|
4
|
+
:width="_cpCanvasScaler(cWidth, pixelRatio, 0)"
|
|
5
|
+
:height="_cpCanvasScaler(pHeight, pixelRatio, 0)"
|
|
6
|
+
:style="canvasStyle" />
|
|
7
|
+
<slot name="axisCanvas">
|
|
8
|
+
</slot>
|
|
9
|
+
<slot name="annCanvas">
|
|
10
|
+
</slot>
|
|
11
|
+
<canvas id="plotArea" class="canvas" ref="plotArea"
|
|
12
|
+
:width="_cpCanvasScaler(cWidth, pixelRatio, 0)"
|
|
13
|
+
:height="_cpCanvasScaler(pHeight, pixelRatio, 0)"
|
|
14
|
+
:style="canvasStyle">
|
|
15
|
+
</canvas>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
<script>
|
|
19
|
+
import {
|
|
20
|
+
compose,
|
|
21
|
+
head,
|
|
22
|
+
propEq,
|
|
23
|
+
propOr,
|
|
24
|
+
path,
|
|
25
|
+
findIndex,
|
|
26
|
+
sum,
|
|
27
|
+
map,
|
|
28
|
+
take,
|
|
29
|
+
sortBy,
|
|
30
|
+
prop
|
|
31
|
+
} from 'ramda'
|
|
32
|
+
|
|
33
|
+
import protobuf from 'protobufjs';
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
name: 'TimeseriesPlotCanvas',
|
|
37
|
+
|
|
38
|
+
props: {
|
|
39
|
+
cHeight: Number,
|
|
40
|
+
cWidth: Number,
|
|
41
|
+
start: Number,
|
|
42
|
+
duration: Number,
|
|
43
|
+
constants: Object,
|
|
44
|
+
rsPeriod: Number,
|
|
45
|
+
ts_start: Number,
|
|
46
|
+
ts_end: Number,
|
|
47
|
+
globalZoomMult: Number,
|
|
48
|
+
},
|
|
49
|
+
watch: {
|
|
50
|
+
rsPeriod: function() {
|
|
51
|
+
this.invalidate();
|
|
52
|
+
this.requestedPages.clear();
|
|
53
|
+
},
|
|
54
|
+
viewerMontageScheme: function () {
|
|
55
|
+
this.sendMontageMessage(this.viewerMontageScheme)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
},
|
|
59
|
+
computed: {
|
|
60
|
+
activeViewer: function() {
|
|
61
|
+
return this.$store.getters.activeViewer
|
|
62
|
+
},
|
|
63
|
+
viewerChannels: function() {
|
|
64
|
+
return this.$store.getters.viewerChannels
|
|
65
|
+
},
|
|
66
|
+
viewerMontageScheme: function() {
|
|
67
|
+
return this.$store.getters.viewerMontageScheme
|
|
68
|
+
},
|
|
69
|
+
canvasStyle: function() {
|
|
70
|
+
return {
|
|
71
|
+
width: this.cWidth + 'px',
|
|
72
|
+
height: this.pHeight + 'px'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
pHeight: function () {
|
|
76
|
+
return this.cHeight - 20
|
|
77
|
+
},
|
|
78
|
+
timeSeriesUrl: function() {
|
|
79
|
+
return `wss://${process.env.VUE_APP_PENNSIEVE_API_LOCATION}/streaming/discover/ts/query?session=` + this.userToken + '&package=' + encodeURIComponent(this.packageId)
|
|
80
|
+
},
|
|
81
|
+
packageId: function() {
|
|
82
|
+
return propOr('', 'packageId', this.activeViewer)
|
|
83
|
+
},
|
|
84
|
+
userToken: function() {
|
|
85
|
+
return this.$store.getters.userToken
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
data: function () {
|
|
89
|
+
return {
|
|
90
|
+
proto: {
|
|
91
|
+
'nested': {
|
|
92
|
+
'Event': {
|
|
93
|
+
'fields': {
|
|
94
|
+
'source': {
|
|
95
|
+
'type': 'string',
|
|
96
|
+
'id': 1
|
|
97
|
+
},
|
|
98
|
+
'pageStart': {
|
|
99
|
+
'type': 'uint64',
|
|
100
|
+
'id': 2
|
|
101
|
+
},
|
|
102
|
+
'pageEnd': {
|
|
103
|
+
'type': 'uint64',
|
|
104
|
+
'id': 3
|
|
105
|
+
},
|
|
106
|
+
'samplePeriod': {
|
|
107
|
+
'type': 'double',
|
|
108
|
+
'id': 4
|
|
109
|
+
},
|
|
110
|
+
'pointsPerEvent': {
|
|
111
|
+
'type': 'uint64',
|
|
112
|
+
'id': 5
|
|
113
|
+
},
|
|
114
|
+
'isResampled': {
|
|
115
|
+
'type': 'bool',
|
|
116
|
+
'id': 6
|
|
117
|
+
},
|
|
118
|
+
'data': {
|
|
119
|
+
'rule': 'repeated',
|
|
120
|
+
'type': 'double',
|
|
121
|
+
'id': 7
|
|
122
|
+
},
|
|
123
|
+
'times': {
|
|
124
|
+
'rule': 'repeated',
|
|
125
|
+
'type': 'uint64',
|
|
126
|
+
'id': 8
|
|
127
|
+
},
|
|
128
|
+
'spikeGroup': {
|
|
129
|
+
'rule': 'repeated',
|
|
130
|
+
'type': 'uint32',
|
|
131
|
+
'id': 9
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
'Instruction': {
|
|
136
|
+
'fields': {
|
|
137
|
+
'command': {
|
|
138
|
+
'type': 'string',
|
|
139
|
+
'id': 1
|
|
140
|
+
},
|
|
141
|
+
'argument': {
|
|
142
|
+
'type': 'string',
|
|
143
|
+
'id': 2
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
'Segment': {
|
|
148
|
+
'fields': {
|
|
149
|
+
'startTs': {
|
|
150
|
+
'type': 'uint64',
|
|
151
|
+
'id': 1
|
|
152
|
+
},
|
|
153
|
+
'source': {
|
|
154
|
+
'type': 'string',
|
|
155
|
+
'id': 2
|
|
156
|
+
},
|
|
157
|
+
'lastUsed': {
|
|
158
|
+
'type': 'uint64',
|
|
159
|
+
'id': 3
|
|
160
|
+
},
|
|
161
|
+
'unit': {
|
|
162
|
+
'type': 'string',
|
|
163
|
+
'id': 4
|
|
164
|
+
},
|
|
165
|
+
'samplePeriod': {
|
|
166
|
+
'type': 'double',
|
|
167
|
+
'id': 5
|
|
168
|
+
},
|
|
169
|
+
'requestedSamplePeriod': {
|
|
170
|
+
'type': 'double',
|
|
171
|
+
'id': 6
|
|
172
|
+
},
|
|
173
|
+
'pageStart': {
|
|
174
|
+
'type': 'uint64',
|
|
175
|
+
'id': 7
|
|
176
|
+
},
|
|
177
|
+
'isMinMax': {
|
|
178
|
+
'type': 'bool',
|
|
179
|
+
'id': 8
|
|
180
|
+
},
|
|
181
|
+
'unitM': {
|
|
182
|
+
'type': 'uint64',
|
|
183
|
+
'id': 9
|
|
184
|
+
},
|
|
185
|
+
'segmentType': {
|
|
186
|
+
'type': 'string',
|
|
187
|
+
'id': 10
|
|
188
|
+
},
|
|
189
|
+
'nrPoints': {
|
|
190
|
+
'type': 'uint64',
|
|
191
|
+
'id': 11
|
|
192
|
+
},
|
|
193
|
+
'data': {
|
|
194
|
+
'rule': 'repeated',
|
|
195
|
+
'type': 'double',
|
|
196
|
+
'id': 12
|
|
197
|
+
},
|
|
198
|
+
'pageEnd': {
|
|
199
|
+
'type': 'uint64',
|
|
200
|
+
'id': 13
|
|
201
|
+
},
|
|
202
|
+
'channelName': {
|
|
203
|
+
'type': 'string',
|
|
204
|
+
'id': 14
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
'IngestSegment': {
|
|
209
|
+
'fields': {
|
|
210
|
+
'channelId': {
|
|
211
|
+
'type': 'string',
|
|
212
|
+
'id': 1
|
|
213
|
+
},
|
|
214
|
+
'startTime': {
|
|
215
|
+
'type': 'uint64',
|
|
216
|
+
'id': 2
|
|
217
|
+
},
|
|
218
|
+
'samplePeriod': {
|
|
219
|
+
'type': 'double',
|
|
220
|
+
'id': 3
|
|
221
|
+
},
|
|
222
|
+
'data': {
|
|
223
|
+
'rule': 'repeated',
|
|
224
|
+
'type': 'double',
|
|
225
|
+
'id': 4
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
'TimeSeriesMessage': {
|
|
230
|
+
'fields': {
|
|
231
|
+
'segment': {
|
|
232
|
+
'type': 'Segment',
|
|
233
|
+
'id': 3
|
|
234
|
+
},
|
|
235
|
+
'event': {
|
|
236
|
+
'rule': 'repeated',
|
|
237
|
+
'type': 'Event',
|
|
238
|
+
'id': 4
|
|
239
|
+
},
|
|
240
|
+
'instruction': {
|
|
241
|
+
'type': 'Instruction',
|
|
242
|
+
'id': 5
|
|
243
|
+
},
|
|
244
|
+
'ingestSegment': {
|
|
245
|
+
'type': 'IngestSegment',
|
|
246
|
+
'id': 6
|
|
247
|
+
},
|
|
248
|
+
'totalResponses': {
|
|
249
|
+
'type': 'uint64',
|
|
250
|
+
'id': 7
|
|
251
|
+
},
|
|
252
|
+
'responseSequenceId': {
|
|
253
|
+
'type': 'uint64',
|
|
254
|
+
'id': 8
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
pixelRatio: 1,
|
|
261
|
+
requestedPages: Object,
|
|
262
|
+
chData: [],
|
|
263
|
+
aSyncrequests: [],
|
|
264
|
+
aSyncPreRequests: [],
|
|
265
|
+
pageSize: 15000000,
|
|
266
|
+
viewData: {
|
|
267
|
+
start: 0,
|
|
268
|
+
duration: 0,
|
|
269
|
+
channels: []
|
|
270
|
+
},
|
|
271
|
+
renderCounter: 0,
|
|
272
|
+
lastDataRender: null,
|
|
273
|
+
lastViewPageRequest: null,
|
|
274
|
+
prefetchTimer: Object,
|
|
275
|
+
prefetchDelay: 100,
|
|
276
|
+
autoScale:0,
|
|
277
|
+
channelsReady:false,
|
|
278
|
+
prevStart:0,
|
|
279
|
+
prevDuration:0,
|
|
280
|
+
websocket: Object,
|
|
281
|
+
initWebsocket: true,
|
|
282
|
+
segment: null,
|
|
283
|
+
timeSeriesMessage:null,
|
|
284
|
+
timeSeriesError:null,
|
|
285
|
+
channelsList:null,
|
|
286
|
+
catchUpRequestPages:null,
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
mounted: function () {
|
|
291
|
+
|
|
292
|
+
this.requestedPages = new Map();
|
|
293
|
+
this.chData = [];
|
|
294
|
+
this.aSyncrequests = [];
|
|
295
|
+
this.throttledGetRenderData = this.throttle(this.renderDataOnMessage, 250, {leading:false});
|
|
296
|
+
this.throttledDataRender = this.throttle(this._renderData, 50);
|
|
297
|
+
|
|
298
|
+
// Timer loop to prefetch pages --> initiated in requestData()
|
|
299
|
+
const that = this;
|
|
300
|
+
this.preFetchRequestFnc = function() {
|
|
301
|
+
const nrPending = that.aSyncPreRequests.length;
|
|
302
|
+
if (nrPending > 0) {
|
|
303
|
+
if (that.requestedPages.size < 3) {
|
|
304
|
+
that.requestDataFromServer([that.aSyncPreRequests[0]]);
|
|
305
|
+
that.aSyncPreRequests.splice(0, 1);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
clearInterval(that.prefetchTimer);
|
|
309
|
+
that.isPrefetching = false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const protobufInstance = protobuf.Root.fromJSON(this.proto)
|
|
314
|
+
this.segment = protobufInstance.Segment;
|
|
315
|
+
this.timeSeriesMessage = protobufInstance.TimeSeriesMessage
|
|
316
|
+
this.timeSeriesError = protobufInstance.TimeSeriesError
|
|
317
|
+
this.channelsList = protobufInstance.ChannelsList
|
|
318
|
+
|
|
319
|
+
this.requestedPages = new Map();
|
|
320
|
+
this.openWebsocket()
|
|
321
|
+
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
methods: {
|
|
325
|
+
|
|
326
|
+
throttle: function(func, wait, options) {
|
|
327
|
+
let context;
|
|
328
|
+
let args;
|
|
329
|
+
let result;
|
|
330
|
+
let timeout = null;
|
|
331
|
+
let previous = 0;
|
|
332
|
+
if (!options) options = {};
|
|
333
|
+
const later = function() {
|
|
334
|
+
previous = options.leading === false ? 0 : Date.now();
|
|
335
|
+
timeout = null;
|
|
336
|
+
result = func.apply(context, args);
|
|
337
|
+
if (!timeout) context = args = null;
|
|
338
|
+
};
|
|
339
|
+
return function() {
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
if (!previous && options.leading === false) previous = now;
|
|
342
|
+
const remaining = wait - (now - previous);
|
|
343
|
+
context = this;
|
|
344
|
+
args = arguments;
|
|
345
|
+
if (remaining <= 0 || remaining > wait) {
|
|
346
|
+
if (timeout) {
|
|
347
|
+
clearTimeout(timeout);
|
|
348
|
+
timeout = null;
|
|
349
|
+
}
|
|
350
|
+
previous = now;
|
|
351
|
+
result = func.apply(context, args);
|
|
352
|
+
if (!timeout) context = args = null;
|
|
353
|
+
} else if (!timeout && options.trailing !== false) {
|
|
354
|
+
timeout = setTimeout(later, remaining);
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
viewSegmComparator: function(a, b) {
|
|
360
|
+
if (a.startTs< b.startTs) return -1;
|
|
361
|
+
if (a.startTs > b.startTs) return 1;
|
|
362
|
+
return 0;
|
|
363
|
+
},
|
|
364
|
+
_cpCanvasScaler: function(sz, pixelRatio, offset) {
|
|
365
|
+
return pixelRatio * (sz + offset);
|
|
366
|
+
},
|
|
367
|
+
getScreenPixelRatio: function() {
|
|
368
|
+
let ctx = this.$refs.plotArea.getContext('2d');
|
|
369
|
+
let dpr = window.devicePixelRatio || 1
|
|
370
|
+
let bsr = ctx.webkitBackingStorePixelRatio ||
|
|
371
|
+
ctx.mozBackingStorePixelRatio ||
|
|
372
|
+
ctx.msBackingStorePixelRatio ||
|
|
373
|
+
ctx.oBackingStorePixelRatio ||
|
|
374
|
+
ctx.backingStorePixelRatio || 1;
|
|
375
|
+
|
|
376
|
+
return dpr / bsr;
|
|
377
|
+
},
|
|
378
|
+
getChannelId: function(channel) {
|
|
379
|
+
const isViewingMontage = this.viewerMontageScheme !== 'NOT_MONTAGED'
|
|
380
|
+
let id = propOr('', 'id', channel)
|
|
381
|
+
let list = []
|
|
382
|
+
if (isViewingMontage) {
|
|
383
|
+
list = id.split('_')
|
|
384
|
+
id = list.length ? head(list) : id // remove channel name from id
|
|
385
|
+
}
|
|
386
|
+
return id
|
|
387
|
+
},
|
|
388
|
+
initChannels: function(channels) {
|
|
389
|
+
const chObjects = [];
|
|
390
|
+
if (channels.length > 0) {
|
|
391
|
+
|
|
392
|
+
const channelConfig = []
|
|
393
|
+
|
|
394
|
+
for (let ic=0; ic<channels.length; ic++) {
|
|
395
|
+
const curC = channels[ic].content;
|
|
396
|
+
const curId = this.getChannelId(curC)
|
|
397
|
+
const curChannel = {
|
|
398
|
+
id: curId,
|
|
399
|
+
label: curC.name,
|
|
400
|
+
type: curC.channelType,
|
|
401
|
+
segments: [],
|
|
402
|
+
start: curC.start,
|
|
403
|
+
end: curC.end,
|
|
404
|
+
sampleFreq: curC.rate,
|
|
405
|
+
unit: curC.unit,
|
|
406
|
+
gaps: [], //channels[ic].gaps
|
|
407
|
+
virtualId: curC.virtualId
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const label = curChannel.label.split(/[ _-]/, 3)
|
|
411
|
+
const label_prefix = label[0];
|
|
412
|
+
let label_value = ( (label.length > 1) ? parseFloat(label[1]) : 0);
|
|
413
|
+
label_value = ( (isNaN(label_value) ? label[1] : label_value));
|
|
414
|
+
|
|
415
|
+
channelConfig.push({
|
|
416
|
+
id: curChannel.id, // id of channel
|
|
417
|
+
type: curChannel.type, // Type of channel (CONTINUOUS, UNIT)
|
|
418
|
+
label: curChannel.label, // Label of channel
|
|
419
|
+
label_split: label, // array of label segments
|
|
420
|
+
label_prefix: label_prefix, // prefix for label
|
|
421
|
+
label_value: label_value, // value for label
|
|
422
|
+
dataSegments: [], // vector of segments rnages (start end ...)
|
|
423
|
+
rank: ic, // rank for determining display order
|
|
424
|
+
visible: true, // channel visible in viewer?
|
|
425
|
+
plotAgainst: null, // for montaging
|
|
426
|
+
rowBaseline: null, // offset for centering
|
|
427
|
+
rowScale: 1, // for individual zoom
|
|
428
|
+
rowAdjust: 0, // offset for centering
|
|
429
|
+
selected: false, // Item is selected
|
|
430
|
+
hover: false, // User is hovering over channel
|
|
431
|
+
unit: curC.unit, // unit of data
|
|
432
|
+
sf: curC.rate, // sampling rate
|
|
433
|
+
filter: {}, // filter object (type, var0, var1, notch)
|
|
434
|
+
hideFilter: true,
|
|
435
|
+
isEditing: false,
|
|
436
|
+
virtualId: curChannel.virtualId
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
chObjects.push(curChannel);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
channelConfig.sort(function(a, b) {
|
|
443
|
+
|
|
444
|
+
// If split into more than 2 segments, just sort on entire string
|
|
445
|
+
if (a.label_split.length > 2 || b.label_split.length > 2) {
|
|
446
|
+
return (a.label > b.label) ? 1 : ((b.label > a.label) ? -1 : 0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// If prefix is similar, sort on value
|
|
450
|
+
if (a.label_prefix === b.label_prefix) {
|
|
451
|
+
return (a.label_value > b.label_value) ? 1 : ((b.label_value > a.label_value) ? -1 : 0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Sort on prefix
|
|
455
|
+
return (a.label_prefix > b.label_prefix) ? 1 : ((b.label_prefix > a.label_prefix) ? -1 : 0);
|
|
456
|
+
} );
|
|
457
|
+
|
|
458
|
+
// Update rank on sorted channels
|
|
459
|
+
for (let i = 0; i < channelConfig.length; i++) {
|
|
460
|
+
channelConfig[i].rank = i;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
this.$store.dispatch('setChannels', channelConfig)
|
|
464
|
+
this.$emit('channelsInitialized', channels)
|
|
465
|
+
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.computeSummary(chObjects)
|
|
469
|
+
this.chData = chObjects;
|
|
470
|
+
this.autoScale = channels.length;
|
|
471
|
+
this.channelsReady = true;
|
|
472
|
+
|
|
473
|
+
return Promise.resolve()
|
|
474
|
+
|
|
475
|
+
},
|
|
476
|
+
computeSummary: function(channels) {
|
|
477
|
+
|
|
478
|
+
if (channels.length === 0) {
|
|
479
|
+
this.globalGaps = null
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.globalGaps = path(['gaps', 0], channels);
|
|
484
|
+
|
|
485
|
+
},
|
|
486
|
+
// Find segment that contains timepoint val --> search over startPage property
|
|
487
|
+
segmIndexOf: function(segmArray, val, first, startAtIndex) {
|
|
488
|
+
|
|
489
|
+
if (!startAtIndex) {
|
|
490
|
+
startAtIndex = 0;
|
|
491
|
+
}
|
|
492
|
+
let index;
|
|
493
|
+
index = this._indexOfStart(segmArray, val, startAtIndex, segmArray.length - 1, first);
|
|
494
|
+
|
|
495
|
+
if (index === -1) {
|
|
496
|
+
index = 0;
|
|
497
|
+
} else if (index < 0) {
|
|
498
|
+
index = -index - 2;
|
|
499
|
+
}
|
|
500
|
+
return index;
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
// Find segment offset in segment array
|
|
504
|
+
_indexOfStart: function(segmArray, val, min, max, firstIndex) {
|
|
505
|
+
|
|
506
|
+
/*
|
|
507
|
+
Return code status:
|
|
508
|
+
Positive number: first index of the value
|
|
509
|
+
Negative 1 (-1): belongs before the start of the dataset
|
|
510
|
+
Other negative value: belongs after (-value - 2)
|
|
511
|
+
*/
|
|
512
|
+
if (max < min) {
|
|
513
|
+
let pred;
|
|
514
|
+
if (max >= 0) {
|
|
515
|
+
//return -max - 2;
|
|
516
|
+
pred = max;
|
|
517
|
+
} else {
|
|
518
|
+
//return max;
|
|
519
|
+
pred = -max - 2;
|
|
520
|
+
}
|
|
521
|
+
if (pred === -1) {
|
|
522
|
+
return pred;
|
|
523
|
+
}
|
|
524
|
+
const predVal = segmArray[pred].pageStart;
|
|
525
|
+
while (pred >= 0 && segmArray[pred].pageStart === predVal) {
|
|
526
|
+
pred--;
|
|
527
|
+
}
|
|
528
|
+
pred++;
|
|
529
|
+
return -pred - 2;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const mid = parseInt((min + max) / 2);
|
|
533
|
+
|
|
534
|
+
if (segmArray[mid].pageStart > val) {
|
|
535
|
+
return this._indexOfStart(segmArray, val, min, mid - 1, firstIndex);
|
|
536
|
+
} else if (segmArray[mid].pageStart < val) {
|
|
537
|
+
return this._indexOfStart(segmArray, val, mid + 1, max, firstIndex);
|
|
538
|
+
} else {
|
|
539
|
+
let index = mid;
|
|
540
|
+
if (firstIndex) {
|
|
541
|
+
while (index >= 0 && segmArray[index].pageStart === val) {
|
|
542
|
+
index--;
|
|
543
|
+
}
|
|
544
|
+
index++;
|
|
545
|
+
} else {
|
|
546
|
+
while (index < segmArray.length && segmArray[index].pageStart === val) {
|
|
547
|
+
index++;
|
|
548
|
+
}
|
|
549
|
+
index--;
|
|
550
|
+
}
|
|
551
|
+
return index;
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
// Initiate render of data --> check visible channels and create viewData object.
|
|
555
|
+
generatePoints: function() {
|
|
556
|
+
|
|
557
|
+
// If not initialized --> return
|
|
558
|
+
if (!this.viewerChannels) {
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.viewData.start = this.start;
|
|
563
|
+
this.viewData.duration = this.duration;
|
|
564
|
+
|
|
565
|
+
// Get hidden channels and view-ids
|
|
566
|
+
const hiddenChannels = [];
|
|
567
|
+
const showChannels = [];
|
|
568
|
+
const curChanViewIds = [];
|
|
569
|
+
|
|
570
|
+
// Get IDS from channelConfig
|
|
571
|
+
for (let i = 0; i < this.viewerChannels.length; i++) {
|
|
572
|
+
const curConfig = this.viewerChannels[i];
|
|
573
|
+
const id = this.getChannelId(curConfig.id)
|
|
574
|
+
if (!curConfig.visible) {
|
|
575
|
+
hiddenChannels.push(id);
|
|
576
|
+
}
|
|
577
|
+
curChanViewIds.push(id);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Get IDS from viewData channels
|
|
581
|
+
const viewDataChIds = [];
|
|
582
|
+
for (let i = 0; i< this.viewData.channels.length; i++) {
|
|
583
|
+
const channel = this.viewData.channels[i]
|
|
584
|
+
const channelId = this.getChannelId(channel)
|
|
585
|
+
viewDataChIds.push(channelId);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Iterate over all channels and populate channelConfig
|
|
589
|
+
const remViewChannels = [];
|
|
590
|
+
for (let iChan = 0; iChan < this.chData.length; iChan++) {
|
|
591
|
+
const curChan = this.chData[iChan];
|
|
592
|
+
const curChanId = this.getChannelId(curChan)
|
|
593
|
+
// Only return visible channels
|
|
594
|
+
if (hiddenChannels.indexOf(curChanId) >= 0) {
|
|
595
|
+
remViewChannels.push(curChanId);
|
|
596
|
+
} else {
|
|
597
|
+
showChannels.push(curChan);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// removing channels from viewData when not shown.
|
|
602
|
+
// Iterate backwards to prevent issues of indexing
|
|
603
|
+
remViewChannels.sort();
|
|
604
|
+
for (let i=(remViewChannels.length-1); i>=0; i--) {
|
|
605
|
+
this.splice('viewData.channels', remViewChannels[i], 1);
|
|
606
|
+
viewDataChIds.splice(remViewChannels[i], 1);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Get viewDataChannels
|
|
610
|
+
for (let i=0; i< showChannels.length; i++) {
|
|
611
|
+
const curShowChannel = showChannels[i]
|
|
612
|
+
const showChannelId = this.getChannelId(curShowChannel);
|
|
613
|
+
const idx = viewDataChIds.indexOf(showChannelId);
|
|
614
|
+
if (idx < 0 ) {
|
|
615
|
+
this.viewData.channels.push({
|
|
616
|
+
id: showChannelId,
|
|
617
|
+
mean: null,
|
|
618
|
+
firstRenderedIndex: 0,
|
|
619
|
+
lastRenderedIndex: 0,
|
|
620
|
+
blocks: []
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Request data on visible channels
|
|
627
|
+
// Only request when pageForward key is released
|
|
628
|
+
// if (!this.spacePressed && this.selectedTab === 'timeseries') {
|
|
629
|
+
this.requestData(showChannels, this.start, this.duration);
|
|
630
|
+
// }
|
|
631
|
+
},
|
|
632
|
+
// Getting data from cache and queue new reqeusts from server
|
|
633
|
+
requestData: function(showChannels, start, duration ) {
|
|
634
|
+
|
|
635
|
+
// If number of requestedpages is getting to big, reset websocket connection
|
|
636
|
+
|
|
637
|
+
// TODO: BRING BACK
|
|
638
|
+
// if (this.requestedPages.size > 20 ) {
|
|
639
|
+
// clearTimeout(this.preFetchTime);
|
|
640
|
+
// this.aSyncPreRequests = [];
|
|
641
|
+
// this.reRequestPages();
|
|
642
|
+
// this.openWebsocket();
|
|
643
|
+
// return;
|
|
644
|
+
// }
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
// Init asnch requests for viewport pages
|
|
648
|
+
this.aSyncRequests = [];
|
|
649
|
+
|
|
650
|
+
// If we rerender the same viewport (as data comes in) --> don't change prefetch
|
|
651
|
+
let updatePrefetchPages = false
|
|
652
|
+
if (this.prevStart !== start || this.prevDuration !== duration) {
|
|
653
|
+
this.aSyncPreRequests= []
|
|
654
|
+
updatePrefetchPages = true
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
this.prevStart = start;
|
|
658
|
+
this.prevDuration = duration;
|
|
659
|
+
|
|
660
|
+
// Timestamp of first value in viewport
|
|
661
|
+
const viewDataChIds = [];
|
|
662
|
+
for (let i=0; i<this.viewData.channels.length; i++) {
|
|
663
|
+
const cChannel = this.viewData.channels[i]
|
|
664
|
+
viewDataChIds.push(cChannel);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Iterate over channels and populate aSyncRequests and/or chanViewData
|
|
668
|
+
for (let iChan = 0; iChan < showChannels.length; iChan++) {
|
|
669
|
+
let continuationSegment = false;
|
|
670
|
+
const curChan = showChannels[iChan];
|
|
671
|
+
const chDataSegments = curChan.segments;
|
|
672
|
+
const curChanId = this.getChannelId(curChan)
|
|
673
|
+
// Get dataView
|
|
674
|
+
const idx = findIndex(propEq('id', curChanId), viewDataChIds)
|
|
675
|
+
const chanViewData = this.viewData.channels[idx];
|
|
676
|
+
chanViewData.blocks = [];
|
|
677
|
+
|
|
678
|
+
// Populate chanViewData until all data in scope is added, or requested from server.
|
|
679
|
+
// Independent of actual request, populate viewData by pageSegments.
|
|
680
|
+
let curTime = Math.floor(start/ this.pageSize) * this.pageSize;
|
|
681
|
+
if (curTime < 0) {
|
|
682
|
+
curTime = 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
let firstSegment = 0;
|
|
686
|
+
let segmOffset = 0;
|
|
687
|
+
if (chDataSegments.length > 0) {
|
|
688
|
+
firstSegment = this.segmIndexOf(chDataSegments, curTime, true, 0);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Iterate over blocks in viewport and add to chanView, or asyncRequests
|
|
692
|
+
let endRequestTime = start + duration + this.constants['PREFETCHPAGES']*this.pageSize;
|
|
693
|
+
if (endRequestTime > this.ts_end) {
|
|
694
|
+
endRequestTime = this.ts_end;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
let curSegm;
|
|
698
|
+
if (chDataSegments.length > 0) {
|
|
699
|
+
curSegm = chDataSegments[firstSegment];
|
|
700
|
+
}
|
|
701
|
+
while ((curTime < endRequestTime) || continuationSegment) {
|
|
702
|
+
continuationSegment = false;
|
|
703
|
+
|
|
704
|
+
// If current time == start of current page in cache then page is correct.
|
|
705
|
+
let inRange = false;
|
|
706
|
+
if (curSegm) {
|
|
707
|
+
inRange = curTime >= curSegm.pageStart && curTime <= curSegm.pageEnd;
|
|
708
|
+
}
|
|
709
|
+
if (inRange) {
|
|
710
|
+
/*
|
|
711
|
+
Data is already cached
|
|
712
|
+
*/
|
|
713
|
+
let isViewPage = curSegm.startTs < start + duration;
|
|
714
|
+
|
|
715
|
+
segmOffset += 1;
|
|
716
|
+
//curTime = curSegm.startTs + curSegm.nrPoints*curSegm.samplePeriod;
|
|
717
|
+
curTime = curSegm.pageEnd;
|
|
718
|
+
|
|
719
|
+
// Only add to viewData if segment is not a prefetch page
|
|
720
|
+
if (isViewPage) {
|
|
721
|
+
chanViewData.blocks.push(curSegm);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const prevPageTime = curSegm.pageStart;
|
|
725
|
+
curSegm = chDataSegments[firstSegment + segmOffset];
|
|
726
|
+
if (!this.isStreaming && curSegm && prevPageTime === curSegm.pageStart) {
|
|
727
|
+
continuationSegment = true;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
} else {
|
|
731
|
+
/*
|
|
732
|
+
Data is not present on client and needs to be requested from server
|
|
733
|
+
*/
|
|
734
|
+
|
|
735
|
+
// Check if segment is already being requested
|
|
736
|
+
if (this.requestedPages.get(curTime)) {
|
|
737
|
+
curTime += this.pageSize;
|
|
738
|
+
if (curSegm && curTime >= curSegm.pageEnd) {
|
|
739
|
+
while (curSegm.pageEnd < curTime) {
|
|
740
|
+
segmOffset += 1;
|
|
741
|
+
curSegm = chDataSegments[firstSegment + segmOffset];
|
|
742
|
+
if (!curSegm) {
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let isViewPage = curTime < start + duration;
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
// Check if requested range is already requested by other channel --> update
|
|
754
|
+
let isAdded = false;
|
|
755
|
+
if (isViewPage) {
|
|
756
|
+
|
|
757
|
+
//remove from pre-request
|
|
758
|
+
for (let iA in this.aSyncPreRequests) {
|
|
759
|
+
if (this.aSyncPreRequests[iA].start === curTime) {
|
|
760
|
+
this.aSyncPreRequests.splice(iA, 1);
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
for (let iA in this.aSyncRequests) {
|
|
767
|
+
if (this.aSyncRequests[iA].start === curTime) {
|
|
768
|
+
this.aSyncRequests[iA].channels.push(curChan);
|
|
769
|
+
isAdded = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (!isAdded) {
|
|
773
|
+
this.aSyncRequests.push({
|
|
774
|
+
channels: [curChan],
|
|
775
|
+
start: curTime,
|
|
776
|
+
duration: this.pageSize,
|
|
777
|
+
isInViewport: true,
|
|
778
|
+
pixelWidth: Math.ceil(this.rsPeriod)});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
} else {
|
|
783
|
+
if (updatePrefetchPages) {
|
|
784
|
+
for (let iA in this.aSyncPreRequests) {
|
|
785
|
+
if (this.aSyncPreRequests[iA].start === curTime) {
|
|
786
|
+
this.aSyncPreRequests[iA].channels.push(curChan);
|
|
787
|
+
isAdded = true;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if(!isAdded) {
|
|
791
|
+
this.aSyncPreRequests.push({
|
|
792
|
+
channels: [curChan],
|
|
793
|
+
start: curTime,
|
|
794
|
+
duration: this.pageSize,
|
|
795
|
+
isInViewport: false,
|
|
796
|
+
pixelWidth: Math.ceil(this.rsPeriod)});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
curTime += this.pageSize;
|
|
801
|
+
if (curSegm && curTime >= curSegm.pageEnd) {
|
|
802
|
+
while (curSegm.pageEnd < curTime) {
|
|
803
|
+
segmOffset += 1;
|
|
804
|
+
curSegm = chDataSegments[firstSegment + segmOffset];
|
|
805
|
+
if (!curSegm) {
|
|
806
|
+
break;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Sort the ChannelView pages
|
|
814
|
+
chanViewData.blocks.sort(this.viewSegmComparator);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Directly request all pages that are in the viewport
|
|
818
|
+
if (this.aSyncRequests.length > 0) {
|
|
819
|
+
|
|
820
|
+
// order requests to make sure request with start in viewport is first.
|
|
821
|
+
let firstRequest = 0;
|
|
822
|
+
for (let i=0; i<this.aSyncRequests.length; i++) {
|
|
823
|
+
if (this.aSyncRequests[i].start >= this.start && this.aSyncRequests[i].start < (this.start +this.duration)) {
|
|
824
|
+
firstRequest = i;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
this.requestDataFromServer(this.aSyncRequests, firstRequest);
|
|
829
|
+
this.lastViewPageRequest = Date.now();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Requests Pre-fetch pages throttled, after viewport pages are available
|
|
833
|
+
if (this.aSyncPreRequests.length > 0 ) {
|
|
834
|
+
if (!this.isPrefetching) {
|
|
835
|
+
this.prefetchTimer = setInterval(this.preFetchRequestFnc, 150);
|
|
836
|
+
this.isPrefetching = true;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
// Request pages from the Server
|
|
841
|
+
requestDataFromServer: function(requests, firstRequest = 0) {
|
|
842
|
+
// Do the actual Websocket requests.
|
|
843
|
+
if (requests.length > 0) {
|
|
844
|
+
|
|
845
|
+
const datasetEndTime = this.ts_end;
|
|
846
|
+
// check for last block
|
|
847
|
+
for(let i = 0; i < requests.length; i++) {
|
|
848
|
+
let curRequest;
|
|
849
|
+
if (i === 0 ) {
|
|
850
|
+
curRequest = requests[firstRequest];
|
|
851
|
+
} else if (i === firstRequest) {
|
|
852
|
+
curRequest = requests[0];
|
|
853
|
+
} else {
|
|
854
|
+
curRequest = requests[i];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// check for last block
|
|
858
|
+
let requestEndTime = curRequest.start + curRequest.duration;
|
|
859
|
+
if (requestEndTime > datasetEndTime) {
|
|
860
|
+
requestEndTime = datasetEndTime;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const ws = this._websocket;
|
|
864
|
+
if (ws && ws.readyState === 1 && curRequest.pixelWidth>0) {
|
|
865
|
+
|
|
866
|
+
const virtualChannels = curRequest.channels.map(channel => {
|
|
867
|
+
return { id: channel.id, name: channel.label, }
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
const req = {
|
|
871
|
+
session: this.userToken,
|
|
872
|
+
minMax: true,
|
|
873
|
+
startTime: curRequest.start,
|
|
874
|
+
endTime: requestEndTime,
|
|
875
|
+
packageId: this.packageId,
|
|
876
|
+
pixelWidth: curRequest.pixelWidth,
|
|
877
|
+
virtualChannels
|
|
878
|
+
};
|
|
879
|
+
const reqJson = JSON.stringify(req);
|
|
880
|
+
ws.send(reqJson);
|
|
881
|
+
|
|
882
|
+
// ChannelCounter will capture number of expected packages for each channel
|
|
883
|
+
// This number is set when first package arrives.
|
|
884
|
+
const nrChannels = curRequest.channels.length;
|
|
885
|
+
const channelCounter = new Map();
|
|
886
|
+
for (let j=0; j < nrChannels; j++) {
|
|
887
|
+
channelCounter.set(curRequest.channels[j].id, NaN);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
this.requestedPages.set(curRequest.start,
|
|
891
|
+
{ count: nrChannels,
|
|
892
|
+
counter: channelCounter,
|
|
893
|
+
subPageCount: NaN,
|
|
894
|
+
ts: (Date.now() + 550),
|
|
895
|
+
inViewport: curRequest.isInViewport} );
|
|
896
|
+
|
|
897
|
+
} else {
|
|
898
|
+
console.log('Websocket Closed')
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
this.aSyncrequests = [];
|
|
903
|
+
}
|
|
904
|
+
return true;
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
invalidate: function() {
|
|
908
|
+
console.log('Invalidating')
|
|
909
|
+
|
|
910
|
+
clearTimeout(this.preFetchTime);
|
|
911
|
+
this.aSyncPreRequests = [];
|
|
912
|
+
this.globalGaps = [];
|
|
913
|
+
this.requestedPages.clear()
|
|
914
|
+
for (let i=0; i<this.chData.length; i++) {
|
|
915
|
+
this.chData[i].segments = [];
|
|
916
|
+
}
|
|
917
|
+
for (let i=0; i<this.viewData.channels.length; i++) {
|
|
918
|
+
this.viewData.channels[i].blocks = [];
|
|
919
|
+
}
|
|
920
|
+
this.renderAll()
|
|
921
|
+
},
|
|
922
|
+
// _________________
|
|
923
|
+
// RENDER METHODS
|
|
924
|
+
|
|
925
|
+
renderAll: function() {
|
|
926
|
+
this.generatePoints();
|
|
927
|
+
this._renderData();
|
|
928
|
+
},
|
|
929
|
+
// computes the channelConfig properties based on current settings of viewer.
|
|
930
|
+
_computeChannelViews: function() {
|
|
931
|
+
|
|
932
|
+
// find channel order based on rank
|
|
933
|
+
const mapped = this.viewerChannels.map(function(el, i) { return { index: i, value: el.rank }; })
|
|
934
|
+
mapped.sort(function(a, b) {return +(a.value > b.value) || +(a.value === b.value) - 1;});
|
|
935
|
+
const rankedIds = mapped.map(function(el) {return el.index;});
|
|
936
|
+
const interChannelDist = this.pHeight / this.nrVisibleChannels;
|
|
937
|
+
|
|
938
|
+
// update properties
|
|
939
|
+
// TODO: Bring back
|
|
940
|
+
let curIdx = 0;
|
|
941
|
+
let updatedBaseline = null
|
|
942
|
+
for (let i = 0; i < rankedIds.length; i++) {
|
|
943
|
+
const curRow = this.viewerChannels[rankedIds[i]];
|
|
944
|
+
const oldBaseline = curRow.rowBaseline
|
|
945
|
+
if (curRow.visible) {
|
|
946
|
+
updatedBaseline = (0.5*interChannelDist) + curIdx * interChannelDist;
|
|
947
|
+
// curRow.rowBaseline = (0.5*interChannelDist) + curIdx * interChannelDist;
|
|
948
|
+
curIdx++;
|
|
949
|
+
}
|
|
950
|
+
if (oldBaseline != updatedBaseline) {
|
|
951
|
+
this.$store.dispatch('updateViewChannel', {
|
|
952
|
+
channelId: curRow.id,
|
|
953
|
+
data: {
|
|
954
|
+
rowBaseline: updatedBaseline
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
// Compute canvas coordinates from datarray
|
|
964
|
+
getPointCoords: function(channelInfo, channelData, isRedraw) {
|
|
965
|
+
// Iterate over channelData on single channel ---> channelData are continuous in time
|
|
966
|
+
let segmLength = channelData.blocks.length;
|
|
967
|
+
|
|
968
|
+
// find mean of rendered data
|
|
969
|
+
switch (channelInfo.type) {
|
|
970
|
+
case 'UNIT':
|
|
971
|
+
if (!isRedraw) {
|
|
972
|
+
for (let iSegm = 0; iSegm < segmLength; iSegm++) {
|
|
973
|
+
|
|
974
|
+
const curSeg = channelData.blocks[iSegm];
|
|
975
|
+
|
|
976
|
+
// Iterate over points in segment and set cx, cy, and cy2
|
|
977
|
+
const curCData = curSeg.cData;
|
|
978
|
+
const curData = curSeg.parsedData;
|
|
979
|
+
const xOffset = this.constants['XOFFSET'];
|
|
980
|
+
|
|
981
|
+
// create local variable for speed
|
|
982
|
+
const length = curSeg.parsedData[0].length;
|
|
983
|
+
const cXArray = curCData[0];
|
|
984
|
+
const cYArray = curCData[1];
|
|
985
|
+
const cY2Array = curCData[2];
|
|
986
|
+
const XArray = curData[0];
|
|
987
|
+
|
|
988
|
+
// RowBaseline is offset of channel in viewPort
|
|
989
|
+
const rowBaseLine = channelInfo.rowBaseline | 0;
|
|
990
|
+
|
|
991
|
+
// satrtTime
|
|
992
|
+
const startT = this.start;
|
|
993
|
+
|
|
994
|
+
// Pixel Period
|
|
995
|
+
const rsP = this.rsPeriod;
|
|
996
|
+
|
|
997
|
+
const spikeHeigth = (this.cHeight / (2 * (this.nrVisibleChannels + 1))) |0;
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
for(let iPoint = 0; iPoint < length; iPoint++) {
|
|
1001
|
+
|
|
1002
|
+
cXArray[iPoint] = (((xOffset + (XArray[iPoint] - startT ) / (rsP))));
|
|
1003
|
+
cYArray[iPoint] = rowBaseLine - spikeHeigth;
|
|
1004
|
+
cY2Array[iPoint] = rowBaseLine + spikeHeigth;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
break;
|
|
1009
|
+
|
|
1010
|
+
case 'CONTINUOUS':
|
|
1011
|
+
// eslint-disable-next-line
|
|
1012
|
+
let totalPointsInMean = 0
|
|
1013
|
+
|
|
1014
|
+
if (!isRedraw) {
|
|
1015
|
+
channelData.mean = 0;
|
|
1016
|
+
channelData.median = 0;
|
|
1017
|
+
for (let iSegm = 0; iSegm < segmLength; iSegm++) {
|
|
1018
|
+
const curBlock = channelData.blocks[iSegm];
|
|
1019
|
+
if (curBlock.nrValidPoints > 0) {
|
|
1020
|
+
channelData.mean = ( curBlock.sumElem +
|
|
1021
|
+
(totalPointsInMean * channelData.mean)) / (totalPointsInMean + curBlock.nrValidPoints);
|
|
1022
|
+
channelData.median = ( curBlock.median +
|
|
1023
|
+
(totalPointsInMean * channelData.median)) / (totalPointsInMean + curBlock.nrValidPoints);
|
|
1024
|
+
totalPointsInMean += curBlock.nrValidPoints;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
for (var iSegm = 0; iSegm < segmLength; iSegm++) {
|
|
1030
|
+
|
|
1031
|
+
const curSeg = channelData.blocks[iSegm];
|
|
1032
|
+
|
|
1033
|
+
// Find startIndex viewPort
|
|
1034
|
+
let startIndex = Math.floor((this.start - curSeg.startTs ) / curSeg.samplePeriod);
|
|
1035
|
+
curSeg.renderStartIndex = ((startIndex > 0) ? startIndex : 0)
|
|
1036
|
+
|
|
1037
|
+
let endIndex = Math.floor( ((this.start + this.duration) - (curSeg.startTs + (curSeg.nrPoints*curSeg.samplePeriod))/curSeg.samplePeriod ));
|
|
1038
|
+
curSeg.renderEndIndex = endIndex >= 0 ? (curSeg.nrPoints-1) : (curSeg.nrPoints + endIndex);
|
|
1039
|
+
|
|
1040
|
+
// console.log('start: ' + this.start + " duration: " + this.duration)
|
|
1041
|
+
// console.log('render-start: ' + curSeg.renderStartIndex + " render-end: " + curSeg.renderEndIndex)
|
|
1042
|
+
|
|
1043
|
+
// Iterate over points in segment and set cx, cy, and cy2
|
|
1044
|
+
const curCData = curSeg.cData;
|
|
1045
|
+
const curData = curSeg.parsedData;
|
|
1046
|
+
const curScale = this.globalZoomMult * channelInfo.rowScale;
|
|
1047
|
+
const xOffset = this.constants['XOFFSET'];
|
|
1048
|
+
|
|
1049
|
+
// create local variable for speed
|
|
1050
|
+
const length = curSeg.parsedData[0].length;
|
|
1051
|
+
const cXArray = curCData[0];
|
|
1052
|
+
const cYArray = curCData[1];
|
|
1053
|
+
const cY2Array = curCData[2];
|
|
1054
|
+
const XArray = curData[0];
|
|
1055
|
+
const YArray = curData[1];
|
|
1056
|
+
const Y2Array = curData[2];
|
|
1057
|
+
|
|
1058
|
+
// RowBaseline is offset of channel in viewPort
|
|
1059
|
+
const rowBaseLine = channelInfo.rowBaseline;
|
|
1060
|
+
|
|
1061
|
+
// chDatMean is mean value of channel in viewport
|
|
1062
|
+
let chDatCenterer = 0;
|
|
1063
|
+
if (this.constants['USEMEDIAN']) {
|
|
1064
|
+
chDatCenterer = channelData.median;
|
|
1065
|
+
} else {
|
|
1066
|
+
chDatCenterer = channelData.mean;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const rsp = this.rsPeriod;
|
|
1070
|
+
const startT = this.start;
|
|
1071
|
+
for(let iPoint = 0; iPoint < length; iPoint++) {
|
|
1072
|
+
|
|
1073
|
+
cXArray[iPoint] = (((xOffset + (XArray[iPoint] - startT ) / rsp)));
|
|
1074
|
+
// console.log('xOffset: ' + xOffset + " XArray[ipoint]: " + XArray[iPoint] + " startT: " + startT )
|
|
1075
|
+
// console.log('cXArray[ipoint]: ' + cXArray[iPoint])
|
|
1076
|
+
cYArray[iPoint] = (((rowBaseLine + (YArray[iPoint] - chDatCenterer) * curScale)));
|
|
1077
|
+
|
|
1078
|
+
if (curSeg.isMinMax) {
|
|
1079
|
+
|
|
1080
|
+
// If min and max are the same --> force 1 pixel line.
|
|
1081
|
+
if (YArray[iPoint] === Y2Array[iPoint]) {
|
|
1082
|
+
cY2Array[iPoint] = cYArray[iPoint] + 1;
|
|
1083
|
+
}else {
|
|
1084
|
+
cY2Array[iPoint] = (((rowBaseLine + (Y2Array[iPoint] - chDatCenterer) * curScale)));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
_renderData: function(isRedraw=false) {
|
|
1093
|
+
const ba = this.$refs.blurArea;
|
|
1094
|
+
const ctxb = ba.getContext('2d');
|
|
1095
|
+
ctxb.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
|
|
1096
|
+
ctxb.fillStyle = 'rgb(220,220,220)'
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
const pa = this.$refs.plotArea;
|
|
1100
|
+
const ctx = pa.getContext('2d');
|
|
1101
|
+
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
|
|
1102
|
+
|
|
1103
|
+
// clear canvas
|
|
1104
|
+
ctx.clearRect(0, 0, this.cWidth, this.cHeight);
|
|
1105
|
+
ctxb.clearRect(0, 0, this.cWidth, this.cHeight);
|
|
1106
|
+
|
|
1107
|
+
// update number of visible channels
|
|
1108
|
+
const nrVisCh = this.viewerChannels.reduce((accumulator, currentValue) => {
|
|
1109
|
+
if(currentValue.visible) {
|
|
1110
|
+
return accumulator + 1
|
|
1111
|
+
}
|
|
1112
|
+
return accumulator
|
|
1113
|
+
}, 0)
|
|
1114
|
+
|
|
1115
|
+
this.nrVisibleChannels = nrVisCh;
|
|
1116
|
+
|
|
1117
|
+
// Update label Div
|
|
1118
|
+
// this.$.labelTemplate.render();
|
|
1119
|
+
|
|
1120
|
+
// Compute the channel views for returned data
|
|
1121
|
+
this._computeChannelViews();
|
|
1122
|
+
|
|
1123
|
+
ctx.save();
|
|
1124
|
+
// let allTimes = 0;
|
|
1125
|
+
for (let ch in this.viewData.channels ) {
|
|
1126
|
+
// eslint-disable-next-line
|
|
1127
|
+
if (this.viewData.channels.hasOwnProperty(ch)) {
|
|
1128
|
+
// const chTimeStart = new Date().getTime();
|
|
1129
|
+
|
|
1130
|
+
const curChannelData = this.viewData.channels[ch];
|
|
1131
|
+
|
|
1132
|
+
// Get channelview for current channel
|
|
1133
|
+
let curChannelView = null;
|
|
1134
|
+
curChannelView = this.viewerChannels.find((elem) => {
|
|
1135
|
+
return elem.id === curChannelData.id
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
if(!curChannelView || !curChannelView.visible) {
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Render segment placeholders
|
|
1143
|
+
const startT = this.start;
|
|
1144
|
+
const rsP = this.rsPeriod;
|
|
1145
|
+
const xOffset = this.constants['XOFFSET'];
|
|
1146
|
+
const blurHeight = Math.min( Math.round((this.cHeight/this.viewData.channels.length)- 2), 10);
|
|
1147
|
+
for (let i = 0; i < curChannelView.dataSegments.length; i+=2) {
|
|
1148
|
+
if (curChannelView.dataSegments[i + 1] < startT) {
|
|
1149
|
+
continue
|
|
1150
|
+
}
|
|
1151
|
+
if (curChannelView.dataSegments[i] > (startT + this.duration)) {
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
let xPos1 = Math.floor((((xOffset + (curChannelView.dataSegments[i] - startT ) / (rsP)))));
|
|
1155
|
+
let xPos2 = Math.floor((((xOffset + (curChannelView.dataSegments[i + 1] - startT ) / (rsP)))));
|
|
1156
|
+
let yPos = Math.floor(curChannelView.rowBaseline - blurHeight/2);
|
|
1157
|
+
ctxb.fillRect(xPos1, yPos, xPos2-xPos1, blurHeight);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Get canvas points for current channel
|
|
1161
|
+
this.getPointCoords(curChannelView, curChannelData, isRedraw);
|
|
1162
|
+
|
|
1163
|
+
// check Channel-Type
|
|
1164
|
+
const nrBlocks = curChannelData.blocks.length;
|
|
1165
|
+
let channelType = 'Continuous';
|
|
1166
|
+
if (nrBlocks) {
|
|
1167
|
+
channelType = curChannelData.blocks[0].type;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// draw channel segments.
|
|
1171
|
+
if(curChannelView.hover) {
|
|
1172
|
+
if (curChannelView.selected) {
|
|
1173
|
+
ctx.strokeStyle = 'rgba(39,96,255,0.70)';
|
|
1174
|
+
ctx.fillStyle = 'rgba(39,96,255,0.70)';
|
|
1175
|
+
} else {
|
|
1176
|
+
ctx.strokeStyle = 'rgba(39,96,255,0.60)';
|
|
1177
|
+
ctx.fillStyle = 'rgba(39,96,255,0.60)';
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
} else if(curChannelView.selected) {
|
|
1181
|
+
ctx.strokeStyle = 'rgb(39,96,255)';
|
|
1182
|
+
ctx.fillStyle = 'rgb(39,96,255)';
|
|
1183
|
+
} else if (channelType === 'Neural') {
|
|
1184
|
+
ctx.strokeStyle = 'rgb(120,120,120)';
|
|
1185
|
+
} else {
|
|
1186
|
+
ctx.strokeStyle = 'black';
|
|
1187
|
+
ctx.fillStyle = 'black';
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
let lastBlockEnd = null;
|
|
1191
|
+
let doPolFill = true;
|
|
1192
|
+
let realSamplePeriod = 1000000 * (1/curChannelView.sf);
|
|
1193
|
+
|
|
1194
|
+
for (let block = 0; block < nrBlocks; block++) {
|
|
1195
|
+
|
|
1196
|
+
const curBlock = curChannelData.blocks[block];
|
|
1197
|
+
|
|
1198
|
+
if (curBlock.nrPoints === 0) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const curData = curBlock.cData;
|
|
1203
|
+
const curDataLength = curBlock.nrPoints;
|
|
1204
|
+
const xVec = curData[0];
|
|
1205
|
+
const yVec = curData[1];
|
|
1206
|
+
const y2Vec = curData[2];
|
|
1207
|
+
|
|
1208
|
+
let startIndex = curBlock.renderStartIndex;
|
|
1209
|
+
let endIndex = curBlock.renderEndIndex
|
|
1210
|
+
|
|
1211
|
+
ctxb.clearRect(Math.floor(xVec[startIndex]), Math.floor(curChannelView.rowBaseline - blurHeight/2), Math.ceil(xVec[endIndex]-xVec[startIndex] + 2), blurHeight+1);
|
|
1212
|
+
|
|
1213
|
+
// Check if minMax and render data
|
|
1214
|
+
switch (curBlock.type) {
|
|
1215
|
+
case 'Continuous':
|
|
1216
|
+
case 'realtime':
|
|
1217
|
+
|
|
1218
|
+
if (curBlock.isMinMax) {
|
|
1219
|
+
/*
|
|
1220
|
+
Data is served as min/max per timespan. Based on render settings
|
|
1221
|
+
we render polygon through points, or render vertical lines.
|
|
1222
|
+
Default is rendering polygon for accuracy.
|
|
1223
|
+
*/
|
|
1224
|
+
|
|
1225
|
+
if ((curBlock.samplePeriod / realSamplePeriod) < 3) {
|
|
1226
|
+
doPolFill = false;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (doPolFill) {
|
|
1230
|
+
|
|
1231
|
+
ctx.beginPath();
|
|
1232
|
+
|
|
1233
|
+
// set cursor to starting point of polygon
|
|
1234
|
+
if (block > 0 ) {//&& !this.isStreaming){
|
|
1235
|
+
if(xVec[startIndex] < (lastBlockEnd.x + 3)) {
|
|
1236
|
+
ctx.moveTo(lastBlockEnd.x, lastBlockEnd.y);
|
|
1237
|
+
} else {
|
|
1238
|
+
ctx.moveTo(xVec[startIndex], yVec[startIndex]);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
} else{
|
|
1242
|
+
ctx.moveTo(xVec[startIndex], yVec[startIndex]);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Draw lines in segment
|
|
1246
|
+
for (let i = startIndex; i < (endIndex+1); i++) {
|
|
1247
|
+
ctx.lineTo(xVec[i], yVec[i]);
|
|
1248
|
+
}
|
|
1249
|
+
for (let i2 = (endIndex-1); i2 >= startIndex; i2--) {
|
|
1250
|
+
ctx.lineTo(xVec[i2], y2Vec[i2]);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Draw line to end of last segment
|
|
1254
|
+
if (block > 0 ) {//&& !this.isStreaming){
|
|
1255
|
+
if(xVec[startIndex] < (lastBlockEnd.x + 3)) {
|
|
1256
|
+
ctx.lineTo(lastBlockEnd.x, lastBlockEnd.y2);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
ctx.closePath();
|
|
1261
|
+
ctx.fill();
|
|
1262
|
+
} else {
|
|
1263
|
+
|
|
1264
|
+
ctx.beginPath();
|
|
1265
|
+
for (let i = startIndex; i < (endIndex+1); i++) {
|
|
1266
|
+
|
|
1267
|
+
ctx.lineTo(xVec[i], yVec[i]);
|
|
1268
|
+
ctx.lineTo(xVec[i], y2Vec[i]);
|
|
1269
|
+
ctx.moveTo(xVec[i], yVec[i]);
|
|
1270
|
+
}
|
|
1271
|
+
ctx.stroke();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
} else {
|
|
1275
|
+
if(block===0) {
|
|
1276
|
+
ctx.beginPath();
|
|
1277
|
+
ctx.moveTo(xVec[startIndex], yVec[startIndex]);
|
|
1278
|
+
} else if(xVec[startIndex] > (lastBlockEnd.x + 2)) {
|
|
1279
|
+
ctx.moveTo(xVec[startIndex], yVec[startIndex]);
|
|
1280
|
+
}
|
|
1281
|
+
for (let i = startIndex; i < (endIndex+1); i++) {
|
|
1282
|
+
ctx.lineTo(xVec[i], yVec[i]);
|
|
1283
|
+
}
|
|
1284
|
+
if (block===(nrBlocks-1)) {
|
|
1285
|
+
ctx.stroke();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
}
|
|
1289
|
+
break;
|
|
1290
|
+
case 'Neural':
|
|
1291
|
+
|
|
1292
|
+
/* is neural event data
|
|
1293
|
+
render timestamps as vertical lines. We are currently assuming
|
|
1294
|
+
single cluster of event --> single color. V2 should include
|
|
1295
|
+
coloring based on cluster number.
|
|
1296
|
+
*/
|
|
1297
|
+
|
|
1298
|
+
// Assume that the data is sorted by cluster
|
|
1299
|
+
ctx.beginPath();
|
|
1300
|
+
for (let i = 0; i < curDataLength; i++) {
|
|
1301
|
+
ctx.moveTo(xVec[i], yVec[i] );
|
|
1302
|
+
ctx.lineTo(xVec[i], y2Vec[i] );
|
|
1303
|
+
}
|
|
1304
|
+
ctx.stroke();
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
lastBlockEnd = {
|
|
1310
|
+
x: xVec[curDataLength -1],
|
|
1311
|
+
y: yVec[curDataLength -1],
|
|
1312
|
+
y2: y2Vec[curDataLength -1]
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// const chTimeEnd = new Date().getTime();
|
|
1318
|
+
// allTimes = allTimes +(chTimeEnd-chTimeStart);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
ctx.restore();
|
|
1322
|
+
// console.log('render time: ' + allTimes)
|
|
1323
|
+
},
|
|
1324
|
+
|
|
1325
|
+
// Function that is called with the throttledDataRenderer
|
|
1326
|
+
renderDataOnMessage: function() {
|
|
1327
|
+
this.generatePoints();
|
|
1328
|
+
if (this.autoScale === 0) {
|
|
1329
|
+
this.autoScale--; // Set to -1 (no autoscale)
|
|
1330
|
+
this.autoScaleViewData();
|
|
1331
|
+
} else {
|
|
1332
|
+
this._renderData();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
},
|
|
1336
|
+
// Method sets the global zoom multiplier based on the viewport data buffer.
|
|
1337
|
+
// This is called once when first page is available
|
|
1338
|
+
autoScaleViewData: function() {
|
|
1339
|
+
console.log('autoscale')
|
|
1340
|
+
let sumMedian = 0;
|
|
1341
|
+
let nrSeg = 0;
|
|
1342
|
+
let allChannels = this.viewData.channels;
|
|
1343
|
+
const allSums = []
|
|
1344
|
+
for (let i=0; i<allChannels.length; i++) {
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
let curBlocks = allChannels[i].blocks;
|
|
1348
|
+
for (let j=0; j<curBlocks.length; j++) {
|
|
1349
|
+
if(curBlocks[j].type !== 'Continuous') {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
sumMedian += this.standardDeviation(curBlocks[j].parsedData[1]);
|
|
1354
|
+
nrSeg++;
|
|
1355
|
+
allSums.push({channel: i, sumMedian})
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const percentage = allSums.length * .80;
|
|
1359
|
+
const median = this.calcSumMedian(percentage, allSums);
|
|
1360
|
+
const avgStd = median / nrSeg;
|
|
1361
|
+
if (!isNaN(avgStd)) {
|
|
1362
|
+
this.$emit('setGlobalZoom', (this.cHeight / allSums.length) / (2 * avgStd))
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
/**
|
|
1366
|
+
* Calculate median sum for list of channels
|
|
1367
|
+
* @param {Number} percentage
|
|
1368
|
+
* @param {Array} list
|
|
1369
|
+
* @returns {Number}
|
|
1370
|
+
*/
|
|
1371
|
+
calcSumMedian: (percentage, list) => compose(
|
|
1372
|
+
sum,
|
|
1373
|
+
map(prop('sumMedian')),
|
|
1374
|
+
take(percentage),
|
|
1375
|
+
sortBy(prop('sumMedian'))
|
|
1376
|
+
)(list),
|
|
1377
|
+
|
|
1378
|
+
// Helper function for autoscale
|
|
1379
|
+
standardDeviation: function(values) {
|
|
1380
|
+
const avg = this.average(values);
|
|
1381
|
+
|
|
1382
|
+
const squareDiffs = values.map(function(value) {
|
|
1383
|
+
const diff = value - avg;
|
|
1384
|
+
const sqrDiff = diff * diff;
|
|
1385
|
+
return sqrDiff;
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
const avgSquareDiff = this.average(squareDiffs);
|
|
1389
|
+
|
|
1390
|
+
const stdDev = Math.sqrt(avgSquareDiff);
|
|
1391
|
+
return stdDev;
|
|
1392
|
+
},
|
|
1393
|
+
// Helper function for autoscale
|
|
1394
|
+
average: function(data) {
|
|
1395
|
+
const sum = data.reduce(function(sum, value) {
|
|
1396
|
+
return sum + value;
|
|
1397
|
+
}, 0);
|
|
1398
|
+
|
|
1399
|
+
const avg = sum / data.length;
|
|
1400
|
+
return avg;
|
|
1401
|
+
},
|
|
1402
|
+
// Get all pages that are partially returned and re-request
|
|
1403
|
+
reRequestPages: function() {
|
|
1404
|
+
|
|
1405
|
+
const requestPages = [];
|
|
1406
|
+
this.requestedPages.forEach(function(value, key) {
|
|
1407
|
+
// Only rerequest pages where we already have partial return
|
|
1408
|
+
if (!isNaN(value.subPageCount)) {
|
|
1409
|
+
|
|
1410
|
+
// only include channels with partial return
|
|
1411
|
+
const channels = [];
|
|
1412
|
+
value.counter.forEach(function(count, chId) {
|
|
1413
|
+
if (!isNaN(count) && count > 0) {
|
|
1414
|
+
channels.push(chId)
|
|
1415
|
+
}
|
|
1416
|
+
})
|
|
1417
|
+
|
|
1418
|
+
if (channels.length > 0) {
|
|
1419
|
+
requestPages.push({
|
|
1420
|
+
channels: channels,
|
|
1421
|
+
start: key,
|
|
1422
|
+
duration: this.pageSize,
|
|
1423
|
+
isInViewport: true,
|
|
1424
|
+
pixelWidth: this.rsPeriod
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}, this);
|
|
1429
|
+
|
|
1430
|
+
// clear requestedPages
|
|
1431
|
+
this.requestedPages.clear();
|
|
1432
|
+
|
|
1433
|
+
this.catchUpRequestPages = requestPages;
|
|
1434
|
+
},
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
// _________________
|
|
1438
|
+
// WEBSOCKET METHODS
|
|
1439
|
+
openWebsocket: function() {
|
|
1440
|
+
//if the websocket is opening or already open, don't open a new one
|
|
1441
|
+
if (this._websocket && (this._websocket.readyState === 0 || this._websocket.readyState === 1) ) {
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
let url = this.timeSeriesUrl
|
|
1446
|
+
this._websocket = new WebSocket(url);
|
|
1447
|
+
this._websocket.onopen = this._onWebsocketOpen.bind(this);
|
|
1448
|
+
this._websocket.onclose = this._onWebsocketClose.bind(this);
|
|
1449
|
+
this._websocket.onmessage = this._onWebsocketMessage.bind(this);
|
|
1450
|
+
|
|
1451
|
+
},
|
|
1452
|
+
sendMontageMessage: function (value) {
|
|
1453
|
+
if (this._websocket && this._websocket.readyState === 1) {
|
|
1454
|
+
const payload = { montage: value, packageId: this.packageId }
|
|
1455
|
+
this._websocket.send(JSON.stringify(payload))
|
|
1456
|
+
} else {
|
|
1457
|
+
this.async(() => {this.sendMontageMessage(value)}, 500)
|
|
1458
|
+
}
|
|
1459
|
+
},
|
|
1460
|
+
_onWebsocketClose: function() {
|
|
1461
|
+
console.log('Websocket closed function')
|
|
1462
|
+
clearTimeout(this.preFetchTime);
|
|
1463
|
+
this.aSyncPreRequests = [];
|
|
1464
|
+
this.openWebsocket();
|
|
1465
|
+
},
|
|
1466
|
+
_onWebsocketFinalClose: function() {
|
|
1467
|
+
console.log('Websocket closed forever');
|
|
1468
|
+
},
|
|
1469
|
+
_onWebsocketOpen: function() {
|
|
1470
|
+
console.log('Websocket is opened')
|
|
1471
|
+
if (this.initWebsocket) {
|
|
1472
|
+
let chIds = [];
|
|
1473
|
+
for (let i=0; i<this.viewerChannels.length; i++) {
|
|
1474
|
+
chIds.push(this.viewerChannels[i].id)
|
|
1475
|
+
}
|
|
1476
|
+
let message = {'channelFiltersToClear':chIds};
|
|
1477
|
+
// clear filters
|
|
1478
|
+
this.sendFilterMessage(message);
|
|
1479
|
+
// clear montage
|
|
1480
|
+
const payload = { montage: 'NOT_MONTAGED', packageId: this.packageId }
|
|
1481
|
+
this._websocket.send(JSON.stringify(payload))
|
|
1482
|
+
this.initWebsocket = false;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
if (this.catchUpRequestPages && this.catchUpRequestPages.length > 0) {
|
|
1486
|
+
this.requestDataFromServer(this.catchUpRequestPages);
|
|
1487
|
+
}
|
|
1488
|
+
this.catchUpRequestPages = [];
|
|
1489
|
+
|
|
1490
|
+
if(this.isStreaming) {
|
|
1491
|
+
this.initStream();
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
this.generatePoints();
|
|
1495
|
+
},
|
|
1496
|
+
_onWebsocketMessage: function(msg) {
|
|
1497
|
+
// process json messages
|
|
1498
|
+
if (typeof msg.data === 'string') {
|
|
1499
|
+
let data = {}
|
|
1500
|
+
|
|
1501
|
+
try {
|
|
1502
|
+
data = JSON.parse(msg.data)
|
|
1503
|
+
} catch (e) {
|
|
1504
|
+
this.$store.dispatch('setViewerErrors', { error: 'JSON Parse Error' })
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (data.channelDetails) {
|
|
1508
|
+
// const baseChannels = this.activeViewer.channels
|
|
1509
|
+
const virtualChannels = data.channelDetails.map(({ id, name, unit, channelType, end, start, rate }) => {
|
|
1510
|
+
// const baseChannel = baseChannels.find(ch => (ch.content.id === id))
|
|
1511
|
+
const content = {
|
|
1512
|
+
id,
|
|
1513
|
+
name,
|
|
1514
|
+
channelType: channelType,
|
|
1515
|
+
label: name,
|
|
1516
|
+
unit: unit,
|
|
1517
|
+
rate: rate,
|
|
1518
|
+
start: start,
|
|
1519
|
+
end: end,
|
|
1520
|
+
virtualId: `${id}_${name}`
|
|
1521
|
+
}
|
|
1522
|
+
// ensure that channel ids are unique for montages
|
|
1523
|
+
if (this.viewerMontageScheme !== 'NOT_MONTAGED') {
|
|
1524
|
+
content.id = `${id}_${name}`
|
|
1525
|
+
}
|
|
1526
|
+
return { content, properties: [] }
|
|
1527
|
+
})
|
|
1528
|
+
|
|
1529
|
+
this.initChannels(virtualChannels)
|
|
1530
|
+
.then(() => {
|
|
1531
|
+
this.invalidate()
|
|
1532
|
+
this.renderAll()
|
|
1533
|
+
})
|
|
1534
|
+
} else if (data.error) {
|
|
1535
|
+
this.$store.dispatch('setViewerErrors', data)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// short circuit
|
|
1539
|
+
return
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// process protobuf messages
|
|
1543
|
+
const myReader = new FileReader();
|
|
1544
|
+
myReader.parent = this;
|
|
1545
|
+
myReader.addEventListener('loadend', function(e) {
|
|
1546
|
+
const buffer = e.srcElement ? e.srcElement.result : e.target.result; //arraybuffer object
|
|
1547
|
+
const barray = new Uint8Array(buffer)
|
|
1548
|
+
|
|
1549
|
+
const timeSeriesMessage = this.parent.timeSeriesMessage.decode(barray);
|
|
1550
|
+
const segment = timeSeriesMessage.segment;
|
|
1551
|
+
|
|
1552
|
+
// Handle Neural Data
|
|
1553
|
+
if (timeSeriesMessage.event && timeSeriesMessage.event.length>0 && timeSeriesMessage.event[0].pageStart) {
|
|
1554
|
+
|
|
1555
|
+
const tsEvent = timeSeriesMessage.event[0];
|
|
1556
|
+
const dataPoints = [[], []];
|
|
1557
|
+
const nrVal = tsEvent.times.length/2;
|
|
1558
|
+
|
|
1559
|
+
let curI = 0;
|
|
1560
|
+
for (let i = 0; i < nrVal; i++) {
|
|
1561
|
+
dataPoints[0].push( tsEvent.times[curI]);
|
|
1562
|
+
dataPoints[1].push( tsEvent.times[curI + 1]);
|
|
1563
|
+
curI += 2;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
let cData = new Array(3);
|
|
1567
|
+
let k=0;
|
|
1568
|
+
while (k < 3) {
|
|
1569
|
+
cData[k] = new Float32Array(dataPoints[0].length);
|
|
1570
|
+
k++;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
const segm = {
|
|
1574
|
+
chId: tsEvent.source,
|
|
1575
|
+
lastUsed: 0,
|
|
1576
|
+
unit: 'uV',
|
|
1577
|
+
samplePeriod: tsEvent.samplePeriod,
|
|
1578
|
+
pageStart: tsEvent.pageStart,
|
|
1579
|
+
pageEnd: tsEvent.pageEnd,
|
|
1580
|
+
startTs: tsEvent.pageStart,
|
|
1581
|
+
isMinMax: tsEvent.isResampled,
|
|
1582
|
+
unitM: 1,
|
|
1583
|
+
type: 'Neural',
|
|
1584
|
+
nrPoints: nrVal,
|
|
1585
|
+
parsedData: dataPoints,
|
|
1586
|
+
cData: cData
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
this.parent.dataCallback({
|
|
1590
|
+
pageStart: tsEvent.pageStart,
|
|
1591
|
+
data: segm,
|
|
1592
|
+
type: 'Neural',
|
|
1593
|
+
nrResponses: timeSeriesMessage.totalResponses
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// Handle Regular Timeseries data package.
|
|
1598
|
+
if (segment !== null) {
|
|
1599
|
+
// Check if viewer is still interested in the arriving data
|
|
1600
|
+
if (segment.requestedSamplePeriod !== Math.ceil(this.parent.rsPeriod)) {
|
|
1601
|
+
return
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// create array from points of data.
|
|
1605
|
+
let nrVal = null;
|
|
1606
|
+
if (segment.isMinMax) {
|
|
1607
|
+
// Create min/max vectors
|
|
1608
|
+
nrVal = segment.data.length/2;
|
|
1609
|
+
} else {
|
|
1610
|
+
//its just one big list of values
|
|
1611
|
+
nrVal = segment.data.length;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
const parsedData = new Array(3);
|
|
1615
|
+
const startTs = segment.startTs
|
|
1616
|
+
|
|
1617
|
+
let sumElem = 0;
|
|
1618
|
+
let nrValidPoints = 0;
|
|
1619
|
+
let i = 0;
|
|
1620
|
+
while (i < 3) {
|
|
1621
|
+
parsedData[i] = new Float64Array(nrVal);
|
|
1622
|
+
i++;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (segment.isMinMax) {
|
|
1626
|
+
// Create min/max vectors
|
|
1627
|
+
let curI = 0;
|
|
1628
|
+
for (let i = 0; i < nrVal; i++) {
|
|
1629
|
+
let curY = -segment.data[curI];
|
|
1630
|
+
let curY2 = -segment.data[curI + 1];
|
|
1631
|
+
parsedData[0][i] = startTs + (i * segment.samplePeriod);
|
|
1632
|
+
parsedData[1][i] = curY;
|
|
1633
|
+
parsedData[2][i] = curY2;
|
|
1634
|
+
if (!isNaN(curY)) {
|
|
1635
|
+
nrValidPoints++;
|
|
1636
|
+
sumElem += curY + (curY2 - curY)/2;
|
|
1637
|
+
}
|
|
1638
|
+
curI += 2;
|
|
1639
|
+
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
} else {
|
|
1643
|
+
//its just one big list of values
|
|
1644
|
+
for (let i = 0; i < nrVal; i++) {
|
|
1645
|
+
let curY = -segment.data[i];
|
|
1646
|
+
parsedData[0][i] = startTs + (i * segment.samplePeriod);
|
|
1647
|
+
parsedData[1][i] = curY;
|
|
1648
|
+
if (!isNaN(curY)) {
|
|
1649
|
+
nrValidPoints++;
|
|
1650
|
+
sumElem += curY;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
let elemMedian = 0;
|
|
1656
|
+
if (this.parent.constants['USEMEDIAN']) {
|
|
1657
|
+
const sortedYvals = Array.prototype.slice.call(parsedData[1]).sort();
|
|
1658
|
+
if (segment.isMinMax) {
|
|
1659
|
+
elemMedian = sortedYvals[Math.round(sortedYvals.length/2)];
|
|
1660
|
+
} else {
|
|
1661
|
+
elemMedian = sortedYvals[Math.round(sortedYvals.length)];
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
let cData = new Array(3);
|
|
1666
|
+
let k=0;
|
|
1667
|
+
while (k < 3) {
|
|
1668
|
+
cData[k] = new Float32Array(parsedData[0].length);
|
|
1669
|
+
k++;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const segm = {
|
|
1673
|
+
chId: segment.source,
|
|
1674
|
+
lastUsed: segment.lastUsed,
|
|
1675
|
+
unit:segment.unit,
|
|
1676
|
+
samplePeriod: segment.samplePeriod,
|
|
1677
|
+
pageStart: segment.pageStart,
|
|
1678
|
+
pageEnd: segment.pageEnd,
|
|
1679
|
+
startTs: startTs,
|
|
1680
|
+
isMinMax: segment.isMinMax,
|
|
1681
|
+
unitM: segment.unitM,
|
|
1682
|
+
type: segment.segmentType,
|
|
1683
|
+
nrPoints: nrVal,
|
|
1684
|
+
cData: cData,
|
|
1685
|
+
parsedData: parsedData,
|
|
1686
|
+
median: elemMedian,
|
|
1687
|
+
sumElem: sumElem,
|
|
1688
|
+
nrValidPoints: nrValidPoints,
|
|
1689
|
+
name: segment.channelName,
|
|
1690
|
+
label: segment.channelName,
|
|
1691
|
+
virtualId: `${segment.source}_${segment.channelName}`
|
|
1692
|
+
};
|
|
1693
|
+
|
|
1694
|
+
if(segm.nrPoints>0) {
|
|
1695
|
+
|
|
1696
|
+
this.parent.dataCallback({
|
|
1697
|
+
pageStart: segment.pageStart,
|
|
1698
|
+
data: segm,
|
|
1699
|
+
type: segment.segmentType,
|
|
1700
|
+
nrResponses: timeSeriesMessage.totalResponses
|
|
1701
|
+
});
|
|
1702
|
+
} else {
|
|
1703
|
+
// Check if already in Gap Array
|
|
1704
|
+
this.parent.dataCallback({
|
|
1705
|
+
pageStart: segment.pageStart,
|
|
1706
|
+
data: segm,
|
|
1707
|
+
nrResponses: timeSeriesMessage.totalResponses,
|
|
1708
|
+
type: 'gap'
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
myReader.readAsArrayBuffer(msg.data);
|
|
1716
|
+
},
|
|
1717
|
+
sendFilterMessage: function(msg) {
|
|
1718
|
+
if (this._websocket && this._websocket.readyState === 1) {
|
|
1719
|
+
const parms = msg.filterParameters || [];
|
|
1720
|
+
parms.forEach( p => {
|
|
1721
|
+
if (!this.isTruthy(p)) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
this._websocket.send(JSON.stringify(msg));
|
|
1726
|
+
} else {
|
|
1727
|
+
this.async(() => {this.sendFilterMessage(msg)}, 200);
|
|
1728
|
+
}
|
|
1729
|
+
},
|
|
1730
|
+
isTruthy: val => val && val !== '' && !val.isNaN,
|
|
1731
|
+
calcFilterType: function(low, high, notch) {
|
|
1732
|
+
switch(true) {
|
|
1733
|
+
case !this.isTruthy(low) && !this.isTruthy(high) && !notch:
|
|
1734
|
+
return 'clear';
|
|
1735
|
+
case this.isTruthy(low) && this.isTruthy(high):
|
|
1736
|
+
return 'bandpass';
|
|
1737
|
+
case this.isTruthy(low):
|
|
1738
|
+
return 'highpass';
|
|
1739
|
+
case this.isTruthy(high):
|
|
1740
|
+
return 'lowpass';
|
|
1741
|
+
default:
|
|
1742
|
+
return 'bandstop';
|
|
1743
|
+
}
|
|
1744
|
+
},
|
|
1745
|
+
// Callback method for async data fetch from server
|
|
1746
|
+
dataCallback: function(obj) {
|
|
1747
|
+
|
|
1748
|
+
// Autoscale counter is set to number > 0 for first request, and autoscale is triggered when
|
|
1749
|
+
// counter reaches 0.
|
|
1750
|
+
if (this.autoScale > 0) {
|
|
1751
|
+
this.autoScale--;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// Find channel for object in chData
|
|
1755
|
+
let curChData = null;
|
|
1756
|
+
for (let ch = 0; ch < this.chData.length; ch++) {
|
|
1757
|
+
if (this.chData[ch].label === obj.data.label) {
|
|
1758
|
+
curChData = this.chData[ch];
|
|
1759
|
+
break;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// Based on Obj type, handle placement in cache differently
|
|
1764
|
+
switch(obj.type) {
|
|
1765
|
+
|
|
1766
|
+
case 'gap':
|
|
1767
|
+
case 'Neural': //neural and continuous data get put in the cache the same way
|
|
1768
|
+
case 'Continuous':
|
|
1769
|
+
|
|
1770
|
+
// Check if data already exists in chData (happens when zoom out, and back in before data is there)
|
|
1771
|
+
// eslint-disable-next-line
|
|
1772
|
+
let addData = false;
|
|
1773
|
+
// eslint-disable-next-line
|
|
1774
|
+
let curSegments = curChData && curChData.segments;
|
|
1775
|
+
if (curSegments && obj.type !== 'gap') {
|
|
1776
|
+
addData = true;
|
|
1777
|
+
if (curSegments.length > 0) {
|
|
1778
|
+
let fIndex = this.segmIndexOf(curSegments, obj.data.startTs, true, 0);
|
|
1779
|
+
|
|
1780
|
+
// Iterate over all segments in CHData with same pageStart to see if page is already present
|
|
1781
|
+
while( curSegments[fIndex] && curSegments[fIndex].pageStart === obj.data.pageStart) {
|
|
1782
|
+
if (curSegments[fIndex].startTs === obj.data.startTs) {
|
|
1783
|
+
addData = false;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
fIndex++;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// Remove returned page from the list of requested Pages.
|
|
1792
|
+
// eslint-disable-next-line
|
|
1793
|
+
let requestedPage = this.requestedPages.get(obj.data.pageStart);
|
|
1794
|
+
// array or page IDs (timestamp, based on viewport)
|
|
1795
|
+
// array of channel IDs
|
|
1796
|
+
if (requestedPage) {
|
|
1797
|
+
|
|
1798
|
+
// Update time
|
|
1799
|
+
requestedPage.ts = Date.now();
|
|
1800
|
+
|
|
1801
|
+
let countForChannel = requestedPage.counter.get(obj.data.chId);
|
|
1802
|
+
if (isNaN(countForChannel)) {
|
|
1803
|
+
countForChannel = obj.nrResponses -1;
|
|
1804
|
+
requestedPage.counter.set(obj.data.chId, countForChannel);
|
|
1805
|
+
requestedPage.subPageCount = countForChannel;
|
|
1806
|
+
} else {
|
|
1807
|
+
countForChannel = countForChannel - 1;
|
|
1808
|
+
requestedPage.counter.set(obj.data.chId, countForChannel);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (countForChannel <= 0) {
|
|
1812
|
+
let isComplete = true;
|
|
1813
|
+
for (let value2 of requestedPage.counter.values()) {
|
|
1814
|
+
if (value2 > 0 || isNaN(value2)) {
|
|
1815
|
+
isComplete = false;
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
if (isComplete) {
|
|
1820
|
+
this.requestedPages.delete(obj.data.pageStart);
|
|
1821
|
+
// console.log('isComplete')
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
// Add data to local cache
|
|
1829
|
+
if (addData) {
|
|
1830
|
+
curSegments.push(obj.data);
|
|
1831
|
+
|
|
1832
|
+
// Resort segments in each channel afer adding data
|
|
1833
|
+
curSegments.sort(function Comparator(a, b) {
|
|
1834
|
+
if (a.startTs < b.startTs) return -1;
|
|
1835
|
+
if (a.startTs > b.startTs) return 1;
|
|
1836
|
+
return 0;
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
// Check if returned page falls in viewport
|
|
1840
|
+
if (obj.pageStart < (this.start + this.duration)) {
|
|
1841
|
+
this.throttledGetRenderData();
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
break;
|
|
1846
|
+
|
|
1847
|
+
case 'realtime':
|
|
1848
|
+
console.log('No Longer Supporting RealTime Data')
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
</script>
|
|
1858
|
+
|
|
1859
|
+
<style lang="scss" scoped>
|
|
1860
|
+
.canvas {
|
|
1861
|
+
position: absolute;
|
|
1862
|
+
top: 0;
|
|
1863
|
+
left: 0;
|
|
1864
|
+
margin-left: 5px;
|
|
1865
|
+
cursor: ew-resize;
|
|
1866
|
+
outline: none;
|
|
1867
|
+
}
|
|
1868
|
+
</style>
|