MIDAS
Loading...
Searching...
No Matches
mihistory.js
Go to the documentation of this file.
1/********************************************************************\
2
3 Name: mihistory.js
4 Created by: Stefan Ritt
5
6 Contents: JavaScript image history routines
7
8 Note: please load midas.js and mhttpd.js before mihistory.js
9
10 \********************************************************************/
11
12LN10 = 2.302585094;
13LOG2 = 0.301029996;
14LOG5 = 0.698970005;
15
16function mihistory_init() {
17 // go through all data-name="mhistory" tags
18 let mhist = Array.from(document.getElementsByTagName("div")).filter(d => {
19 return d.className === "mjsihistory";
20 });
21
22 let baseURL = window.location.href;
23 if (baseURL.indexOf("?cmd") > 0)
24 baseURL = baseURL.substr(0, baseURL.indexOf("?cmd"));
25 baseURL += "?cmd=history";
26
27 for (let i = 0; i < mhist.length; i++) {
28 mhist[i].dataset.baseURL = baseURL;
29 mhist[i].mhg = new MihistoryGraph(mhist[i]);
30 mhist[i].mhg.initializeIPanel(i);
31 mhist[i].mhg.resize();
32 mhist[i].resize = function () {
33 this.mhg.resize();
34 };
35 }
36}
37
38function mihistory_create(parentElement, baseURL, panel, index) {
39 let d = document.createElement("div");
40 parentElement.appendChild(d);
41 d.dataset.baseURL = baseURL;
42 d.dataset.panel = panel;
43 d.mhg = new MihistoryGraph(d);
44 d.mhg.initializeIPanel(index);
45 return d;
46}
47
48function getUrlVars() {
49 let vars = {};
50 window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
51 vars[key] = value;
52 });
53 return vars;
54}
55
56function MihistoryGraph(divElement) { // Constructor
57
58 // create canvas inside the div
59 this.parentDiv = divElement;
60 this.baseURL = divElement.dataset.baseURL;
61 this.panel = divElement.dataset.panel;
62
63 this.imageElem = document.createElement("img");
64 this.imageElem.style.border = "2px solid #808080";
65 this.imageElem.style.margin = "auto";
66 this.imageElem.style.display = "block";
67 this.imageElem.id = "hiImage_" + this.panel;
68
69 this.buttonCanvas = document.createElement("canvas");
70 this.buttonCanvas.width = 30;
71 this.buttonCanvas.height = 30;
72 this.buttonCanvas.style.position = "absolute";
73 this.buttonCanvas.style.top = "0px";
74 this.buttonCanvas.style.right = "0px";
75 this.buttonCanvas.style.zIndex = "11";
76
77 this.axisCanvas = document.createElement("canvas");
78
79 this.fileLabel = document.createElement("div");
80 this.fileLabel.id = "fileLabel_" + this.panel;
81 this.fileLabel.style.backgroundColor = "white";
82 this.fileLabel.style.opacity = "0.7";
83 this.fileLabel.style.fontSize = "10px";
84 this.fileLabel.style.padding = "2px";
85 this.fileLabel.innerHTML = "";
86
87 divElement.style.position = "relative";
88 this.fileLabel.style.position = "absolute";
89 this.fileLabel.style.top = "4px";
90 this.fileLabel.style.right = "34px";
91 this.fileLabel.style.zIndex = "10";
92
93 divElement.appendChild(this.imageElem);
94 divElement.appendChild(this.fileLabel);
95 divElement.appendChild(this.buttonCanvas);
96 divElement.appendChild(this.axisCanvas);
97
98 // colors
99 this.color = {
100 background: "#FFFFFF",
101 axis: "#808080",
102 mark: "#0000A0",
103 label: "#404040",
104 };
105
106 // scales
107 this.tScale = 8*3600;
108 this.tMax = Math.floor(new Date() / 1000);
109 this.tMin = this.tMax - this.tScale;
110 this.scroll = true;
111 this.showZoomButtons = true;
112 this.currentTime = 0;
113 this.currentIndex = 0;
114 this.playMode = 0;
115 this.updatePaused = false;
116
117 // overwrite scale from URL if present
118 this.requestedTime = Math.floor(decodeURI(getUrlVars()["T"]));
119 if (!Number.isNaN(this.requestedTime)) {
120 this.tMax = this.requestedTime;
121 this.tMin = this.requestedTime - this.tScale;
122 }
123
124 // callbacks when certain actions are performed.
125 // All callback functions should accept a single parameter, which is the
126 // MhistoryGraph object that triggered the callback.
127 this.callbacks = {
128 resetAxes: undefined,
129 timeZoom: undefined,
130 jumpToCurrent: undefined
131 };
132
133 // buttons
134 this.button = [
135 {
136 src: "maximize-2.svg",
137 title: "Show only this plot",
138 click: function (t) {
139 window.location.href = t.baseURL + "&group=" + "Images" + "&panel=" + t.panel;
140 }
141 },
142 {
143 src: "chevrons-left.svg",
144 title: "Play backwards",
145 click: function (t) {
146 t.scroll = false;
147 if (t.playMode > -1)
148 t.playMode = -1;
149 else
150 t.playMode *= 2;
151 t.loadOldData();
152 t.play();
153 }
154 },
155 {
156 src: "chevron-left.svg",
157 title: "Step one image back",
158 click: function (t) {
159 t.scroll = false;
160 t.playMode = 0;
161 if (t.currentIndex > 0)
162 t.currentIndex--;
163 t.loadOldData();
164 t.play();
165 }
166 },
167 {
168 src: "pause.svg",
169 title: "Stop playing",
170 click: function (t) {
171 t.scroll = false;
172 t.playMode = 0;
173 t.redraw();
174 }
175 },
176 {
177 src: "chevron-right.svg",
178 title: "Step one image forward",
179 click: function (t) {
180 t.scroll = false;
181 t.playMode = 0;
182 if (t.currentIndex < t.imageArray.length-1)
183 t.currentIndex++;
184 else {
185 t.scroll = true;
186 t.scrollRedraw();
187 }
188 t.loadOldData();
189 t.play();
190 }
191 },
192 {
193 src: "chevrons-right.svg",
194 title: "Play forward",
195 click: function (t) {
196 t.scroll = false;
197 if (t.playMode < 1)
198 t.playMode = 1;
199 else
200 t.playMode *= 2;
201 t.loadOldData();
202 t.play();
203 }
204 },
205 {
206 src: "zoom-in.svg",
207 title: "Zoom in time axis",
208 click: function (t) {
209 t.tScale /= 2;
210 t.tMin += t.tScale;
211 t.drag.Vt = 0 // stop inertia
212 t.redraw();
213 if (t.callbacks.timeZoom !== undefined)
214 t.callbacks.timeZoom(t);
215 }
216 },
217 {
218 src: "zoom-out.svg",
219 title: "Zoom out time axis",
220 click: function (t) {
221 t.tMin -= t.tScale;
222 t.tScale *= 2;
223 t.drag.Vt = 0 // stop inertia
224 t.redraw();
225 t.loadOldData();
226
227 if (t.callbacks.timeZoom !== undefined)
228 t.callbacks.timeZoom(t);
229 }
230 },
231 {
232 src: "play.svg",
233 title: "Jump to last image",
234 click: function (t) {
235 t.playMode = 0;
236 t.scroll = true;
237 t.drag.Vt = 0; // stop inertia
238
239 t.currentIndex = t.imageArray.length-1;
240 t.scrollRedraw();
241
242 if (t.callbacks.jumpToCurrent !== undefined)
243 t.callbacks.jumpToCurrent(t);
244 }
245 },
246 {
247 src: "clock.svg",
248 title: "Select time...",
249 click: function (t) {
250
251 let currentYear = new Date().getFullYear();
252 let dCur = new Date(t.currentTime * 1000);
253
254 if (document.getElementById('y1').length === 0) {
255 for (let i = currentYear; i > currentYear - 5; i--) {
256 let o = document.createElement('option');
257 o.value = i.toString();
258 o.appendChild(document.createTextNode(i.toString()));
259 document.getElementById('y1').appendChild(o);
260 }
261 }
262
263 document.getElementById('m1').selectedIndex = dCur.getMonth();
264 document.getElementById('d1').selectedIndex = dCur.getDate() - 1;
265 document.getElementById('h1').selectedIndex = dCur.getHours();
266 document.getElementById('y1').selectedIndex = currentYear - dCur.getFullYear
267
268 document.getElementById('dlgQueryQuery').onclick = function () {
269 doQueryT(t);
270 }.bind(t);
271
272 dlgShow("dlgQueryT");
273 }
274 },
275 {
276 src: "download.svg",
277 title: "Download current image...",
278 click: function (t) {
279 t.download();
280 }
281 },
282 {
283 src: "help-circle.svg",
284 title: "Show help",
285 click: function () {
286 dlgShow("dlgIHelp", false);
287 }
288 }
289 ];
290
291 // load dialogs
292 dlgLoad('dlgIHistory.html');
293
294 // load button icons
295 this.button.forEach(b => {
296 b.img = new Image();
297 b.img.src = "icons/" + b.src;
298 });
299
300 // mouse event handlers
301 divElement.addEventListener("mousedown", this.mouseEvent.bind(this), true);
302 divElement.addEventListener("dblclick", this.mouseEvent.bind(this), true);
303 divElement.addEventListener("mousemove", this.mouseEvent.bind(this), true);
304 divElement.addEventListener("touchstart", this.mouseEvent.bind(this), true);
305 divElement.addEventListener("touchmove", this.mouseEvent.bind(this), true);
306 divElement.addEventListener("touchend", this.mouseEvent.bind(this), true);
307 divElement.addEventListener("touchcancel", this.mouseEvent.bind(this), true);
308 divElement.addEventListener("mouseup", this.mouseEvent.bind(this), true);
309 divElement.addEventListener("wheel", this.mouseWheelEvent.bind(this), true);
310
311 // Keyboard event handler (has to be on the window!)
312 window.addEventListener("keydown", this.keyDown.bind(this));
313}
314
315function timeToSec(str) {
316 let s = parseFloat(str);
317 switch (str[str.length - 1]) {
318 case 'm':
319 case 'M':
320 s *= 60;
321 break;
322 case 'h':
323 case 'H':
324 s *= 3600;
325 break;
326 case 'd':
327 case 'D':
328 s *= 3600 * 24;
329 break;
330 }
331
332 return s;
333}
334
335function doQueryT(t) {
336
337 dlgHide('dlgQueryT');
338
339 let d = new Date(
340 document.getElementById('y1').value,
341 document.getElementById('m1').selectedIndex,
342 document.getElementById('d1').selectedIndex + 1,
343 document.getElementById('h1').selectedIndex);
344
345 t.scroll = false;
346 t.playMode = 0;
347
348 let tm = d.getTime() / 1000;
349 t.tMax = tm;
350 t.tMin = tm - t.tScale;
351
352 if (tm < t.imageArray[0].time) {
353 t.requestedTime = tm;
354 t.loadOldData();
355 } else {
356 t.setCurrentIndex(tm);
357 }
358
359 if (t.callbacks.timeZoom !== undefined)
360 t.callbacks.timeZoom(t);
361}
362
363
364MihistoryGraph.prototype.keyDown = function (e) {
365 if (e.key === "u") { // 'u' key
366 this.scroll = true;
367 this.scrollRedraw();
368 e.preventDefault();
369 }
370};
371
372MihistoryGraph.prototype.initializeIPanel = function (index) {
373
374 // Retrieve panel
375 this.panel = this.parentDiv.dataset.panel;
376
377 if (this.panel === undefined) {
378 dlgMessage("Error", "Definition of \'dataset-panel\' missing for image history panel \'" + this.parentDiv.id + "\'. " +
379 "Please use syntax:<br /><br /><b>&lt;div class=\"mjsihistory\" " +
380 "data-group=\"&lt;Group&gt;\" data-panel=\"&lt;Panel&gt;\"&gt;&lt;/div&gt;</b>", true);
381 return;
382 }
383
384 if (this.panel === "")
385 return;
386
387 this.index = index;
388 this.marker = {active: false};
389 this.drag = {active: false};
390 this.data = undefined;
391 this.pendingUpdates = 0;
392
393 // image arrays
394 this.imageArray = [];
395
396 // pause main refresh for a moment
397 mhttpd_refresh_pause(true);
398 this.updatePaused = true;
399
400 // retrieve panel definition from ODB
401 mjsonrpc_db_copy(["/History/Images/" + this.panel]).then(function (rpc) {
402 if (rpc.result.status[0] !== 1) {
403 dlgMessage("Error", "Image \'" + this.panel + "\' not found in ODB", true)
404 } else {
405 this.odb = rpc.result.data[0];
406 this.loadInitialData();
407 }
408 }.bind(this)).catch(function (error) {
409 if (error.xhr !== undefined)
410 mjsonrpc_error_alert(error);
411 else
412 throw(error);
413 });
414};
415
416MihistoryGraph.prototype.setCurrentIndex = function(t) {
417 let tmin = Math.abs(t - this.imageArray[0].time);
418 let imin = 0;
419 for (let i = 0; i < this.imageArray.length; i++) {
420 if (Math.abs(t - this.imageArray[i].time) < tmin) {
421 tmin = Math.abs(t - this.imageArray[i].time);
422 imin = i;
423 }
424 }
425 this.currentIndex = imin;
426 this.redraw();
427};
428
429MihistoryGraph.prototype.loadInitialData = function () {
430
431 // get time scale from ODB
432 this.tScale = timeToSec(this.odb["Timescale"]);
433
434 // overwrite via <div ... data-scale=<value> >
435 if (this.parentDiv.dataset.scale !== undefined)
436 this.tScale = timeToSec(this.parentDiv.dataset.scale);
437
438 if (!Number.isNaN(this.requestedTime)) {
439 this.tMax = this.requestedTime;
440 this.tMin = this.requestedTime - this.tScale;
441 this.scroll = false;
442 this.tMinRequested = this.tMax;
443 } else {
444 this.tMax = Math.floor(new Date() / 1000);
445 this.tMin = this.tMax - this.tScale;
446 this.tMinRequested = this.tMax;
447 }
448
449 let table = document.createElement("table");
450 let row = null;
451 let cell;
452 let link;
453
454 // load latest image
455 mjsonrpc_call("hs_image_retrieve",
456 {
457 "image": this.panel,
458 "start_time": Math.floor(this.tMax),
459 "end_time": Math.floor(this.tMax),
460 })
461 .then(function (rpc) {
462
463 this.receiveData(rpc);
464 this.redraw();
465
466 this.updateTimer = window.setTimeout(this.update.bind(this), 1000);
467 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), 1000);
468
469 }.bind(this)).catch(function (error) {
470 mjsonrpc_error_alert(error);
471 });
472
473};
474
475MihistoryGraph.prototype.loadOldData = function () {
476
477 if (this.tMin - this.tScale / 2 < this.tMinRequested) {
478
479 let oldTMinRequested = this.tMinRequested;
480 this.tMinRequested = this.tMin - this.tScale;
481
482 this.pendingUpdates++;
483 this.parentDiv.style.cursor = "progress";
484 mjsonrpc_call("hs_image_retrieve",
485 {
486 "image": this.panel,
487 "start_time": Math.floor(this.tMinRequested),
488 "end_time": Math.floor(oldTMinRequested),
489 })
490 .then(function (rpc) {
491
492 this.pendingUpdates--;
493 if (this.pendingUpdates === 0)
494 this.parentDiv.style.cursor = "default";
495
496 this.receiveData(rpc);
497 this.redraw();
498
499 }.bind(this))
500 .catch(function (error) {
501 mjsonrpc_error_alert(error);
502 });
503 }
504
505 this.redraw();
506};
507
508MihistoryGraph.prototype.loadNextImage = function () {
509 // look from current image backwards for first image not loaded
510 let n = 0;
511 let nParallel = 10; // number of parallel loads
512 for (let i = this.currentIndex; i >= 0; i--) {
513 if (this.imageArray[i].image.src === undefined || this.imageArray[i].image.src === "") {
514 // load up to one window beyond current window
515 if (this.imageArray[i].time > this.tMin - this.tScale) {
516 this.imageArray[i].image.onload = function () {
517 this.mhg.redraw();
518 };
519 this.imageArray[i].image.mhg = this;
520 this.imageArray[i].image.src = this.panel + "/" + this.imageArray[i].image_name;
521 n++;
522 if (n === nParallel) {
523 this.imageArray[i].image.onload = function () {
524 this.mhg.redraw();
525 this.mhg.loadNextImage();
526 };
527 return;
528 }
529 }
530 }
531 }
532
533 // now check images AFTER currentIndex, like if we start with URL T=...
534 for (let i = this.imageArray.length-1 ; i >= 0; i--)
535 if (this.imageArray[i].image.src === undefined || this.imageArray[i].image.src === "") {
536 this.imageArray[i].image.mhg = this;
537 this.imageArray[i].image.src = this.panel + "/" + this.imageArray[i].image_name;
538 n++;
539 if (n === nParallel) {
540 this.imageArray[i].image.onload = function () {
541 this.mhg.loadNextImage();
542 };
543 return;
544 }
545 }
546};
547
548MihistoryGraph.prototype.receiveData = function (rpc) {
549
550 if (rpc.result.data.time.length > 0) {
551 let first = (this.imageArray.length === 0);
552 let lastImageIndex;
553 let newImage = [];
554 let i1 = [];
555
556 // append new values to end of array
557 for (let i = 0; i < rpc.result.data.time.length; i++) {
558 let img = {
559 time: rpc.result.data.time[i],
560 image_name: rpc.result.data.filename[i],
561 image: new Image()
562 }
563 if (this.imageArray.length === 0 ||
564 img.time > this.imageArray[this.imageArray.length - 1].time) {
565 this.imageArray.push(img);
566 newImage.push(img);
567 lastImageIndex = this.imageArray.length - 1;
568 } else if (img.time < this.imageArray[0].time) {
569 i1.push(img);
570 }
571 }
572
573 if (i1.length > 0) {
574 // add new entries to the left side of the array
575 this.imageArray = i1.concat(this.imageArray);
576 this.currentIndex += i1.length;
577 }
578
579 if (this.scroll)
580 this.currentIndex = this.imageArray.length - 1;
581
582 if (!Number.isNaN(this.requestedTime)) {
583 this.setCurrentIndex(this.requestedTime);
584 this.requestedTime = NaN;
585 }
586
587 if (first) {
588 // after loading of fist image, resize panel
589 let img = this.imageArray[this.currentIndex];
590 img.image.onload = function () {
591 this.mhg.imageElem.src = this.src;
592 this.mhg.imageElem.initialWidth = this.width;
593 this.mhg.imageElem.initialHeight = this.height;
594 this.mhg.resize();
595 if (this.mhg.tMinRequested === 0)
596 this.mhg.tMinRequested = this.mhg.tMax;
597
598 // all done, so resume updates
599 mhttpd_refresh_pause(false);
600 this.mhg.updatePaused = false;
601 };
602 img.image.mhg = this;
603 // trigger loading of image
604 img.image.src = this.panel + "/" + img.image_name;
605 } else {
606 // load actual image
607 this.loadNextImage();
608 }
609 }
610};
611
612MihistoryGraph.prototype.update = function () {
613
614 // don't update if we are paused
615 if (this.updatePaused) {
616 this.updateTimer = window.setTimeout(this.update.bind(this), 1000);
617 return;
618 }
619
620 // don't update window if content is hidden (other tab, minimized, etc.)
621 if (document.hidden) {
622 this.updateTimer = window.setTimeout(this.update.bind(this), 500);
623 return;
624 }
625
626 let t = Math.floor(new Date() / 1000);
627 let tStart = this.imageArray[this.imageArray.length-1].time+1;
628
629 mjsonrpc_call("hs_image_retrieve",
630 {
631 "image": this.panel,
632 "start_time": Math.floor(tStart),
633 "end_time": Math.floor(t),
634 })
635 .then(function (rpc) {
636
637 this.receiveData(rpc);
638 this.redraw();
639
640 this.updateTimer = window.setTimeout(this.update.bind(this), 1000);
641
642 }.bind(this)).catch(function (error) {
643 mjsonrpc_error_alert(error);
644 });
645
646};
647
648MihistoryGraph.prototype.scrollRedraw = function () {
649 if (this.scroll) {
650 this.tMax = Math.floor(new Date() / 1000);
651 this.tMin = this.tMax - this.tScale;
652 this.redraw();
653 }
654
655 this.scrollTimer = window.setTimeout(this.scrollRedraw.bind(this), 1000);
656};
657
658MihistoryGraph.prototype.play = function () {
659 window.clearTimeout(this.playTimer);
660
661 let oldIndex = this.currentIndex;
662
663 if (this.playMode > 0) {
664 this.currentIndex += this.playMode;
665 if (this.currentIndex >= this.imageArray.length-1) {
666 this.currentIndex = this.imageArray.length - 1;
667 this.playMode = 0;
668 this.scroll = true;
669 this.scrollRedraw();
670 return;
671 }
672 }
673
674 if (this.playMode < 0) {
675 this.currentIndex += this.playMode;
676 if (this.currentIndex < 0) {
677 this.currentIndex = 0;
678 this.playMode = 0;
679 return;
680 }
681 }
682
683 if (!this.imageArray[this.currentIndex].image.complete)
684 this.currentIndex = oldIndex;
685
686 // shift time axis according to current image
687 this.tMax = this.imageArray[this.currentIndex].time;
688 this.tMin = this.tMax - this.tScale;
689
690 this.redraw();
691 this.loadOldData();
692
693 if (this.callbacks.timeZoom !== undefined)
694 this.callbacks.timeZoom(this);
695
696 if (this.playMode === 0)
697 return;
698
699 this.playTimer = window.setTimeout(this.play.bind(this), 100);
700};
701
702MihistoryGraph.prototype.mouseEvent = function (e) {
703
704 if (e.touches !== undefined) {
705 if (e.touches.length > 1)
706 return;
707 }
708
709 // fix buttons for IE
710 if (!e.which && e.button) {
711 if ((e.button & 1) > 0) e.which = 1; // Left
712 else if ((e.button & 4) > 0) e.which = 2; // Middle
713 else if ((e.button & 2) > 0) e.which = 3; // Right
714 }
715
716 // discard pinch and zoom
717 if (e.type === "touchstart" || e.type === "touchmove" ||
718 e.type === "touchend" || e.type === "touchcancel") {
719 if (e.touches.length > 1)
720 return;
721 }
722
723 // calculate mouse coordinates relative to history panel
724 let rect = document.getElementById("hiImage_" + this.panel).parentElement.getBoundingClientRect();
725 let mouseX, mouseY;
726
727 if (e.type === "touchstart" || e.type === "touchmove" ||
728 e.type === "touchend" || e.type === "touchcancel") {
729 mouseX = Math.floor(e.changedTouches[e.changedTouches.length - 1].clientX - rect.left);
730 mouseY = Math.floor(e.changedTouches[e.changedTouches.length - 1].clientY - rect.top);
731 } else {
732 mouseX = e.clientX - rect.left;
733 mouseY = e.offsetY;
734 }
735
736 let cursor = this.pendingUpdates > 0 ? "progress" : "default";
737 let title = "";
738
739 if (e.type === "mousedown" || e.type === "touchstart") {
740
741 this.loadOldData();
742
743 // check for buttons
744 if (e.target === this.buttonCanvas) {
745 this.button.forEach(b => {
746 if (mouseY > b.y1 && mouseY < b.y1 + b.width &&
747 b.enabled) {
748 cursor = "pointer";
749 b.click(this);
750 }
751 });
752 }
753
754 // start dragging
755 else {
756 cursor = "ew-resize";
757
758 this.drag.active = true;
759 this.scroll = false;
760 this.playMode = 0;
761 this.drag.xStart = mouseX;
762 this.drag.tStart = this.xToTime(mouseX);
763 this.drag.tMinStart = this.tMin;
764 this.drag.tMaxStart = this.tMax;
765 }
766
767 } else if (e.type === "mouseup" || e.type === "touchend" || e.type === "touchcancel") {
768
769 if (this.drag.active) {
770 this.drag.active = false;
771 let now = new Date().getTime();
772 if (this.drag.lastDt !== undefined && now - this.drag.lastT !== 0)
773 this.drag.Vt = this.drag.lastDt / (now - this.drag.lastT);
774 else
775 this.drag.Vt = 0;
776 this.drag.lastMoveT = now;
777 window.setTimeout(this.inertia.bind(this), 50);
778 }
779
780 } else if (e.type === "mousemove" || e.type === "touchmove") {
781
782 // change cursor to arrow over image and axis
783 cursor = "ew-resize";
784
785 if (this.drag.active) {
786
787 // execute dragging
788 cursor = "ew-resize";
789 let dt = (mouseX - this.drag.xStart) / (this.x2 - this.x1) * (this.tMax - this.tMin);
790 this.tMin = this.drag.tMinStart - dt;
791 this.tMax = this.drag.tMaxStart - dt;
792 this.drag.lastDt = (mouseX - this.drag.lastOffsetX) / (this.x2 - this.x1) * (this.tMax - this.tMin);
793 this.drag.lastT = new Date().getTime();
794 this.drag.lastOffsetX = mouseX;
795
796 // don't go into the future
797 if (this.tMax > this.drag.lastT / 1000) {
798 this.tMax = this.drag.lastT / 1000;
799 this.tMin = this.tMax - (this.drag.tMaxStart - this.drag.tMinStart);
800 }
801
802 // seach image closest to current time
803 if (this.imageArray.length > 0)
804 this.setCurrentIndex(this.tMax);
805
806 this.loadOldData();
807
808 if (this.callbacks.timeZoom !== undefined)
809 this.callbacks.timeZoom(this);
810 }
811
812 // change cursor to pointer over buttons
813 if (e.target === this.buttonCanvas) {
814 this.button.forEach(b => {
815 if (e.offsetX > b.x1 && e.offsetY > b.y1 &&
816 e.offsetX < b.x1 + b.width && e.offsetY < b.y1 + b.height) {
817 cursor = "pointer";
818 title = b.title;
819 }
820 });
821 }
822
823 if (this.showZoomButtons) {
824 // check for zoom buttons
825 if (e.offsetX > this.width - 30 - 48 && e.offsetX < this.width - 30 - 24 &&
826 e.offsetY > this.y1 - 24 && e.offsetY < this.y1) {
827 cursor = "pointer";
828 title = "Zoom in";
829 }
830 if (e.offsetX > this.width - 30 - 24 && e.offsetX < this.width - 30 &&
831 e.offsetY > this.y1 - 24 && e.offsetY < this.y1) {
832 cursor = "pointer";
833 title = "Zoom out";
834 }
835 }
836
837 } else if (e.type === "dblclick") {
838
839 }
840
841 this.parentDiv.title = title;
842 this.parentDiv.style.cursor = cursor;
843
844 if (e.touches !== undefined) {
845 if (e.type === "touchmove" && e.touches.length === 1)
846 e.preventDefault();
847 } else
848 // for all mouse events
849 e.preventDefault();
850};
851
852MihistoryGraph.prototype.mouseWheelEvent = function (e) {
853
854 // only use horizontal events
855 if (e.deltaX === 0)
856 return;
857
858 e.preventDefault();
859
860 this.scroll = false;
861 this.playMode = 0;
862
863 this.tMax += e.deltaX * 5;
864 this.tMin = this.tMax - this.tScale;
865
866 // don't go into the future
867 let t = new Date().getTime();
868 if (this.tMax > t / 1000) {
869 this.tMax = t / 1000;
870 this.tMin = this.tMax - this.tScale;
871 }
872
873 // search image closest to current time
874 if (this.imageArray.length > 0)
875 this.setCurrentIndex(this.tMax);
876
877 this.loadOldData();
878};
879
880MihistoryGraph.prototype.inertia = function () {
881 if (this.drag.Vt !== 0) {
882 let now = new Date().getTime();
883 let dt = now - this.drag.lastMoveT;
884 this.drag.lastMoveT = now;
885
886 this.tMin -= this.drag.Vt * dt;
887 this.tMax -= this.drag.Vt * dt;
888
889 this.drag.Vt = this.drag.Vt * 0.85;
890 if (Math.abs(this.drag.Vt) < 0.005) {
891 this.drag.Vt = 0;
892 }
893
894 this.redraw();
895
896 if (this.callbacks.timeZoom !== undefined)
897 this.callbacks.timeZoom(this);
898
899 if (this.drag.Vt !== 0)
900 window.setTimeout(this.inertia.bind(this), 50);
901 }
902};
903
904MihistoryGraph.prototype.setTimespan = function (tMin, tMax, scroll) {
905 this.tMin = tMin;
906 this.tMax = tMax;
907 this.scroll = scroll;
908 this.setCurrentIndex(tMax);
909 this.loadOldData();
910};
911
912MihistoryGraph.prototype.resize = function () {
913 this.width = this.parentDiv.clientWidth;
914 this.height = this.parentDiv.clientHeight;
915
916 this.axisCanvas.width = this.width;
917 this.axisCanvas.height = 30;
918
919 let iAR = 0;
920 let vAR = 0;
921 if (this.imageElem.initialWidth > 0) {
922 iAR = this.imageElem.initialWidth / this.imageElem.initialHeight;
923 vAR = this.width / (this.height - 30);
924 }
925
926 if (iAR === 0) {
927 this.imageElem.width = this.width;
928 this.imageElem.height = this.height - 30;
929 } else {
930 if (iAR < vAR) {
931 this.imageElem.height = this.height - 30 - 4;
932 this.imageElem.width = (this.height - 30) * iAR;
933 } else {
934 this.imageElem.width = this.width-4;
935 this.imageElem.height = this.width / iAR;
936 }
937
938 if (this.imageElem.height + 30 < this.parentDiv.clientHeight) {
939 let diff = this.parentDiv.clientHeight - (this.imageElem.height + 30);
940 this.parentDiv.style.height = (parseInt(this.parentDiv.style.height) - diff) + "px";
941 if (this.parentDiv.parentNode.tagName === "TD") {
942 this.parentDiv.parentNode.style.height = this.parentDiv.style.height;
943 }
944 }
945 }
946 this.buttonCanvas.height = this.imageElem.height;
947
948 this.redraw();
949};
950
951MihistoryGraph.prototype.redraw = function () {
952 let f = this.draw.bind(this);
953 window.requestAnimationFrame(f);
954};
955
956MihistoryGraph.prototype.timeToX = function (t) {
957 return (t - this.tMin) / (this.tMax - this.tMin) * (this.x2 - this.x1) + this.x1;
958};
959
960MihistoryGraph.prototype.xToTime = function (x) {
961 return (x - this.x1) / (this.x2 - this.x1) * (this.tMax - this.tMin) + this.tMin;
962};
963
964MihistoryGraph.prototype.yToValue = function (y) {
965 return (this.y1 - y) / (this.y1 - this.y2) * (this.yMax - this.yMin) + this.yMin;
966};
967
968function convertLastWritten(last) {
969 if (last === 0)
970 return "no data available";
971
972 let d = new Date(last * 1000).toLocaleDateString(
973 'en-GB', {
974 day: '2-digit', month: 'short', year: '2-digit',
975 hour12: false, hour: '2-digit', minute: '2-digit'
976 }
977 );
978
979 return "last data: " + d;
980}
981
982MihistoryGraph.prototype.updateURL = function() {
983 if (this.currentTime > 0) {
984 let url = window.location.href;
985 if (url.search("&T=") !== -1)
986 url = url.slice(0, url.search("&T="));
987 url += "&T=" + Math.round(this.currentTime);
988
989 if (url !== window.location.href)
990 window.history.replaceState({}, "Image History", url);
991 }
992}
993
994MihistoryGraph.prototype.draw = function () {
995
996 // set image from this.currentIndex
997 if (this.imageArray.length > 0) {
998 if (this.imageElem.src !== this.imageArray[this.currentIndex].image.src)
999 this.imageElem.src = this.imageArray[this.currentIndex].image.src;
1000 if (!this.imageArray[this.currentIndex].image.complete)
1001 this.fileLabel.innerHTML = "Loading " + this.imageArray[this.currentIndex].image_name;
1002 else
1003 this.fileLabel.innerHTML = this.imageArray[this.currentIndex].image_name;
1004 this.currentTime = this.imageArray[this.currentIndex].time;
1005 }
1006
1007 // check for valid axis
1008 if (this.tMax === undefined || Number.isNaN(this.tMax))
1009 return;
1010 if (this.tMin === undefined || Number.isNaN(this.tMin))
1011 return;
1012
1013 // don't go into the future
1014 let t = new Date().getTime();
1015 if (this.tMax > t / 1000) {
1016 this.tMax = t / 1000;
1017 this.tMin = this.tMax - this.tScale;
1018 }
1019
1020 // draw time axis
1021 this.x1 = 0;
1022 this.y1 = 30;
1023 this.x2 = this.width-30;
1024 this.y2 = 0;
1025
1026 let ctx = this.axisCanvas.getContext("2d");
1027 ctx.fillStyle = this.color.background;
1028 ctx.fillRect(0, 0, this.width, this.height);
1029
1030 ctx.strokeStyle = this.color.axis;
1031 ctx.drawLine(this.x1, 8, this.x2, 8);
1032 ctx.strokeStyle = "#C00000";
1033 ctx.drawLine(this.x2, 0, this.x2, 30);
1034
1035 ctx.strokeStyle = this.color.axis;
1036 this.drawTAxis(ctx, this.x1, 8, this.x2 - this.x1, this.width,
1037 4, 7, 10, 10, this.tMin, this.tMax);
1038
1039 // marks on time axis
1040 for (let i=0 ; i<this.imageArray.length ; i++) {
1041 let x = this.timeToX(this.imageArray[i].time);
1042 if (this.imageArray[i].image.src !== "" && this.imageArray[i].image.complete)
1043 ctx.strokeStyle = this.color.mark;
1044 else
1045 ctx.strokeStyle = this.color.axis;
1046
1047 ctx.drawLine(x, 0, x, 8);
1048 }
1049
1050 // draw buttons
1051 ctx = this.buttonCanvas.getContext("2d");
1052 if (this.button.length*28+2 < this.buttonCanvas.height)
1053 this.buttonCanvas.height = this.button.length*28+2;
1054
1055 let y = 0;
1056 this.button.forEach(b => {
1057 b.x1 = 1;
1058 b.y1 = 1 + y * 28;
1059 b.width = 28;
1060 b.height = 28;
1061 b.enabled = true;
1062
1063 if (b.src === "maximize-2.svg") {
1064 let s = window.location.href;
1065 if (s.indexOf("&T") > -1)
1066 s = s.substr(0, s.indexOf("&T"));
1067 if (s === encodeURI(this.baseURL + "&group=" + "Images" + "&panel=" + this.panel)) {
1068 b.enabled = false;
1069 return;
1070 }
1071 }
1072
1073 if (b.src === "play.svg" && !this.scroll)
1074 ctx.fillStyle = "#FFC0C0";
1075 else if (b.src === "chevrons-right.svg" && this.playMode > 0)
1076 ctx.fillStyle = "#C0FFC0";
1077 else if (b.src === "chevrons-left.svg" && this.playMode < 0)
1078 ctx.fillStyle = "#C0FFC0";
1079 else
1080 ctx.fillStyle = "#F0F0F0";
1081
1082 ctx.strokeStyle = "#808080";
1083 ctx.fillRect(b.x1, b.y1, b.width, b.height);
1084 ctx.strokeRect(b.x1, b.y1, b.width, b.height);
1085 ctx.drawImage(b.img, b.x1 + 2, b.y1 + 2);
1086
1087 if (b.src === "chevrons-left.svg" && this.playMode < 0) {
1088 ctx.fillStyle = "#404040";
1089 ctx.font = "8px sans-serif";
1090 ctx.fillText("x" + (-this.playMode), b.x1+2, b.y1+b.height-2);
1091 }
1092
1093 if (b.src === "chevrons-right.svg" && this.playMode > 0) {
1094 ctx.fillStyle = "#404040";
1095 ctx.font = "8px sans-serif";
1096 let s = "x" + this.playMode;
1097 ctx.fillText(s, b.x1+b.width-ctx.measureText(s).width-2, b.y1+b.height-2);
1098 }
1099
1100 y++;
1101 });
1102
1103 this.lastDrawTime = new Date().getTime();
1104
1105 // update URL
1106 if (this.updateURLTimer !== undefined)
1107 window.clearTimeout(this.updateURLTimer);
1108
1109 if (this.index === 0)
1110 this.updateURLTimer = window.setTimeout(this.updateURL.bind(this), 500);
1111};
1112
1113let ioptions1 = {
1114 timeZone: 'UTC',
1115 day: '2-digit', month: 'short', year: '2-digit',
1116 hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit'
1117};
1118
1119let ioptions2 = {
1120 timeZone: 'UTC',
1121 day: '2-digit', month: 'short', year: '2-digit',
1122 hour12: false, hour: '2-digit', minute: '2-digit'
1123};
1124
1125let ioptions3 = {
1126 timeZone: 'UTC',
1127 day: '2-digit', month: 'short', year: '2-digit',
1128 hour12: false, hour: '2-digit', minute: '2-digit'
1129};
1130
1131let ioptions4 = {
1132 timeZone: 'UTC', day: '2-digit',
1133 month: 'short', year: '2-digit'
1134};
1135
1136let ioptions5 = {
1137 timeZone: 'UTC', hour12: false,
1138 hour: '2-digit', minute: '2-digit', second: '2-digit'
1139};
1140
1141let ioptions6 = {
1142 timeZone: 'UTC', hour12: false, hour: '2-digit', minute: '2-digit'
1143};
1144
1145let ioptions7 = {
1146 timeZone: 'UTC', hour12: false, hour: '2-digit', minute: '2-digit'
1147};
1148
1149let ioptions8 = {
1150 timeZone: 'UTC',
1151 day: '2-digit', month: 'short', year: '2-digit',
1152 hour12: false, hour: '2-digit', minute: '2-digit'
1153};
1154
1155let ioptions9 = {
1156 timeZone: 'UTC', day: '2-digit', month: 'short', year: '2-digit'
1157};
1158
1159function itimeToLabel(sec, base, forceDate) {
1160 let d = mhttpd_get_display_time(sec).date;
1161
1162 if (forceDate) {
1163 if (base < 60) {
1164 return d.toLocaleTimeString('en-GB', ioptions1);
1165 } else if (base < 600) {
1166 return d.toLocaleTimeString('en-GB', ioptions2);
1167 } else if (base < 3600 * 24) {
1168 return d.toLocaleTimeString('en-GB', ioptions3);
1169 } else {
1170 return d.toLocaleTimeString('en-GB', ioptions4);
1171 }
1172 }
1173
1174 if (base < 60) {
1175 return d.toLocaleTimeString('en-GB', ioptions5);
1176 } else if (base < 600) {
1177 return d.toLocaleTimeString('en-GB', ioptions6);
1178 } else if (base < 3600 * 3) {
1179 return d.toLocaleTimeString('en-GB', ioptions7);
1180 } else if (base < 3600 * 24) {
1181 return d.toLocaleTimeString('en-GB', ioptions8);
1182 } else {
1183 return d.toLocaleTimeString('en-GB', ioptions9);
1184 }
1185
1186 return;
1187}
1188
1189MihistoryGraph.prototype.drawTAxis = function (ctx, x1, y1, width, xr, minor, major,
1190 text, label, xmin, xmax) {
1191 const base = [1, 5, 10, 60, 15 * 60, 30 * 60, 60 * 60, 3 * 60 * 60, 6 * 60 * 60,
1192 12 * 60 * 60, 24 * 60 * 60, 0];
1193
1194 ctx.textAlign = "left";
1195 ctx.textBaseline = "top";
1196
1197 if (xmax <= xmin || width <= 0)
1198 return;
1199
1200 /* force date display if xmax not today */
1201 let d1 = new Date(xmax * 1000);
1202 let d2 = new Date();
1203 let forceDate = d1.getDate() !== d2.getDate() || (d2 - d1 > 1000 * 3600 * 24);
1204
1205 /* use 5 pixel as min tick distance */
1206 let dx = Math.round((xmax - xmin) / (width / 5));
1207
1208 let tick_base;
1209 for (tick_base = 0; base[tick_base]; tick_base++) {
1210 if (base[tick_base] > dx)
1211 break;
1212 }
1213 if (!base[tick_base])
1214 tick_base--;
1215 dx = base[tick_base];
1216
1217 let major_base = tick_base;
1218 let major_dx = dx;
1219
1220 let label_base = major_base;
1221 let label_dx = dx;
1222
1223 do {
1224 let str = itimeToLabel(xmin, label_dx, forceDate);
1225 let maxwidth = ctx.measureText(str).width;
1226
1227 /* increasing label_dx, if labels would overlap */
1228 if (maxwidth > 0.8 * label_dx / (xmax - xmin) * width) {
1229 if (base[label_base + 1])
1230 label_dx = base[++label_base];
1231 else
1232 label_dx += 3600 * 24;
1233
1234 if (label_base > major_base + 1) {
1235 if (base[major_base + 1])
1236 major_dx = base[++major_base];
1237 else
1238 major_dx += 3600 * 24;
1239 }
1240
1241 if (major_base > tick_base + 1) {
1242 if (base[tick_base + 1])
1243 dx = base[++tick_base];
1244 else
1245 dx += 3600 * 24;
1246 }
1247
1248 } else
1249 break;
1250 } while (1);
1251
1252 let d = new Date(xmin * 1000);
1253 let tz = d.getTimezoneOffset() * 60;
1254
1255 let x_act = Math.floor((xmin - tz) / dx) * dx + tz;
1256
1257 ctx.strokeStyle = this.color.axis;
1258 ctx.drawLine(x1, y1, x1 + width, y1);
1259
1260 do {
1261 let x_screen = Math.round((x_act - xmin) / (xmax - xmin) * width + x1);
1262 let xs = Math.round(x_screen);
1263
1264 if (x_screen > x1 + width + 0.001)
1265 break;
1266
1267 if (x_screen >= x1) {
1268 if ((x_act - tz) % major_dx === 0) {
1269 if ((x_act - tz) % label_dx === 0) {
1270 // label tick mark
1271 ctx.strokeStyle = this.color.axis;
1272 ctx.drawLine(xs, y1, xs, y1 + text);
1273
1274 // label
1275 if (label !== 0) {
1276 let str = itimeToLabel(x_act, label_dx, forceDate);
1277
1278 // if labels at edge, don't show them
1279 let xl = xs - ctx.measureText(str).width / 2;
1280 if (xl > 0 && xl + ctx.measureText(str).width < xr) {
1281 ctx.strokeStyle = this.color.label;
1282 ctx.fillStyle = this.color.label;
1283 ctx.fillText(str, xl, y1 + label);
1284 }
1285 }
1286 } else {
1287 // major tick mark
1288 ctx.strokeStyle = this.color.axis;
1289 ctx.drawLine(xs, y1, xs, y1 + major);
1290 }
1291
1292 } else {
1293 // minor tick mark
1294 ctx.strokeStyle = this.color.axis;
1295 ctx.drawLine(xs, y1, xs, y1 + minor);
1296 }
1297 }
1298
1299 x_act += dx;
1300
1301 } while (1);
1302};
1303
1304MihistoryGraph.prototype.download = function () {
1305
1306 let filename = this.imageArray[this.currentIndex].image_name;
1307
1308 // use trick from FileSaver.js
1309 let a = document.getElementById('downloadHook');
1310 if (a === null) {
1311 a = document.createElement("a");
1312 a.style.display = "none";
1313 a.id = "downloadHook";
1314 document.body.appendChild(a);
1315 }
1316
1317 a.href = this.panel + "/" + filename;
1318 a.download = filename;
1319 a.click();
1320 dlgAlert("Image downloaded to '" + filename + "'");
1321};