second commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
padding: 0.25em;
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr 150px;
|
||||
grid-template-rows: 64px 1fr;
|
||||
|
||||
gap: 1em 1em;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"move scanner scan"
|
||||
}
|
||||
.header {
|
||||
align-self: stretch;
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.scanner {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: scanner;
|
||||
}
|
||||
|
||||
.scan {
|
||||
padding: 0 0.25em;
|
||||
align-self: start;
|
||||
grid-area: scan;
|
||||
}
|
||||
.move {
|
||||
align-self: start;
|
||||
grid-area: move;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
.multiwell_cards {
|
||||
display: grid;
|
||||
/*grid-auto-columns: 1fr;
|
||||
grid-auto-rows: 1fr;*/
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
gap: 0.1em;
|
||||
grid-template-areas:
|
||||
". ."
|
||||
". ."
|
||||
". .";
|
||||
}
|
||||
button.multiwell {
|
||||
padding: 0.15em ;
|
||||
}
|
||||
|
||||
#image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--grid-columns, 4), 1fr);
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.5em;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
padding: 0.25em;
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
grid-template-rows: 64px 0.5fr;
|
||||
|
||||
gap: 1em 1em;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"scan scanner"
|
||||
}
|
||||
.header {
|
||||
align-self: stretch;
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.scanner {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: scanner;
|
||||
}
|
||||
|
||||
.scan {
|
||||
align-self: start;
|
||||
grid-area: scan;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
:root{
|
||||
--track-height:10px;
|
||||
--track-color:#ddd;
|
||||
--range-color:#4aa3ff;
|
||||
--thumb-size:18px;
|
||||
--thumb-color:#fff;
|
||||
--thumb-border:#4aa3ff;
|
||||
}
|
||||
|
||||
.slider-container{ width:100%; max-width:640px; margin:32px auto; }
|
||||
.labels{ display:flex; justify-content:space-between; margin-bottom:12px; font-size:14px; }
|
||||
.range-wrap{ position:relative; height:var(--thumb-size); user-select:none; touch-action:none; }
|
||||
.track{ position:absolute; left:0; right:0; top:50%; transform:translateY(-50%); height:var(--track-height); background:var(--track-color); border-radius:999px; }
|
||||
.range-highlight{ position:absolute; top:50%; transform:translateY(-50%); height:var(--track-height); background:var(--range-color); border-radius:999px; }
|
||||
|
||||
input[type=range]{ -webkit-appearance:none; appearance:none; position:absolute; left:0; right:0; top:0; width:100%; height:100%; background:transparent; pointer-events:none; }
|
||||
input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; appearance:none; pointer-events:auto; width:var(--thumb-size); height:var(--thumb-size); border-radius:50%; background:var(--thumb-color); border:2px solid var(--thumb-border); box-shadow:0 1px 3px rgba(0,0,0,0.15); cursor:pointer; margin-top: calc((var(--track-height) - var(--thumb-size)) / 2); }
|
||||
input[type=range]::-moz-range-thumb{ pointer-events:auto; width:var(--thumb-size); height:var(--thumb-size); border-radius:50%; background:var(--thumb-color); border:2px solid var(--thumb-border); box-shadow:0 1px 3px rgba(0,0,0,0.15); cursor:pointer; }
|
||||
input[type=range]::-moz-range-track{ background:transparent; border:0; }
|
||||
|
||||
@media (max-width:520px){ .values{ flex-direction:column; align-items:flex-start; } .readable{ min-width:unset; width:100%; } }
|
||||
@@ -0,0 +1,39 @@
|
||||
|
||||
.multiwell_cards {
|
||||
display: grid;
|
||||
/*grid-auto-columns: 1fr;
|
||||
grid-auto-rows: 1fr;*/
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
gap: 0.1em;
|
||||
grid-template-areas:
|
||||
". ."
|
||||
". ."
|
||||
". .";
|
||||
}
|
||||
button.multiwell {
|
||||
padding: 0.15em ;
|
||||
}
|
||||
#replay-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--grid-columns, 1), 1fr);
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.5em;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.replay-slider, .replay-sub-slide, .replay_ts_cursor{
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.replay-timeline {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
.container {
|
||||
padding: 0.25em;
|
||||
display: grid;
|
||||
grid-template-columns: 0.25fr 1.5fr;
|
||||
grid-template-rows: 0.5fr 3fr 0.5fr;
|
||||
grid-auto-columns: 1fr;
|
||||
gap: 1em 1em;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"header header"
|
||||
"move scanner"
|
||||
"command command";
|
||||
}
|
||||
.header {
|
||||
align-self: stretch;
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.command { grid-area: command; }
|
||||
|
||||
.scanner {
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
grid-area: scanner;
|
||||
}
|
||||
|
||||
.move {
|
||||
align-self: stretch;
|
||||
grid-area: move;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
class ScannerManager {
|
||||
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.socket = null;
|
||||
this.axes = 0;
|
||||
this.cropping = 0;
|
||||
this.debug_count = 0
|
||||
}
|
||||
|
||||
toggle_median() { this.axes = !this.axes; return this.axes; }
|
||||
toggle_crop() { this.croping = !this.croping; return this.croping; }
|
||||
|
||||
init_controls() {
|
||||
this.ts = sId("_ts");
|
||||
const goto_0 = sId("_goto-0");
|
||||
const goto_xy = sId("_goto-xy");
|
||||
const xy_base = sId("_xy-base");
|
||||
const xy_step = sId("_xy-step");
|
||||
const up = sId("_up");
|
||||
const down = sId("_down");
|
||||
const left = sId("_left");
|
||||
const right = sId("_right");
|
||||
this.feed = sId("_feed");
|
||||
this.step = sId("_step");
|
||||
this.well = sId("_well");
|
||||
this.x = sId("_x");
|
||||
this.y = sId("_y");
|
||||
this.dx = sId("_dx");
|
||||
this.dy = sId("_dy");
|
||||
this.xbase = sId("_xbase");
|
||||
this.ybase = sId("_ybase");
|
||||
this.debug = sId("_debug");
|
||||
const test = sId("_test");
|
||||
const halt = sId("_halt");
|
||||
|
||||
const median = sId("_median");
|
||||
const crop = sId("_crop");
|
||||
const crop_radius = sId("_crop_radius");
|
||||
|
||||
up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); });
|
||||
down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); });
|
||||
left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); });
|
||||
right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); });
|
||||
|
||||
goto_0.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_0" }); });
|
||||
goto_xy.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_xy" }); });
|
||||
xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); });
|
||||
xy_step.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_step" }); });
|
||||
|
||||
median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); });
|
||||
crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); });
|
||||
crop_radius.addEventListener('change',(e) => { this._send({ type: 'calibrate', topic: "crop_radius", value: crop_radius.value }); });
|
||||
this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "position", value: e.target.value }); });
|
||||
this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); });
|
||||
this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); });
|
||||
|
||||
this.dx.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dx", value: e.target.value }); });
|
||||
this.dy.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dy", value: e.target.value }); });
|
||||
|
||||
test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
|
||||
halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
this.socket = socket;
|
||||
this.init_controls();
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
try {
|
||||
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
|
||||
if (payload.xbase) { this.xbase.textContent = payload.xbase; this.ybase.textContent = payload.ybase; }
|
||||
if (payload.dxy) { this.dy.value=payload.dy; this.dx.value=payload.dx; }
|
||||
if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
|
||||
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
init() {
|
||||
this.axes = 0;
|
||||
this.cropping = 0;
|
||||
this._send({
|
||||
type: 'calibrate',
|
||||
topic: "init",
|
||||
feed: this.feed.value,
|
||||
step: this.step.value,
|
||||
position: this.well.value
|
||||
});
|
||||
}
|
||||
start() { this._send({ type: 'scanner', topic: "start"}); }
|
||||
halt() { this._send({ type: 'scanner', topic: "halt" }); }
|
||||
|
||||
_send(message) { this.socket.send(message); }
|
||||
}
|
||||
|
||||
class MetadataSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.manager = null;
|
||||
this.reconnectDelay = 1000;
|
||||
this.shouldReconnect = true;
|
||||
this.reconnect = false;
|
||||
}
|
||||
|
||||
setManager(manager) { this.manager = manager; }
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.manager.update(data);
|
||||
};
|
||||
|
||||
this.ws.onopen = (event) => {
|
||||
if (this.manager && !this.reconnect)
|
||||
this.manager['init']();
|
||||
this.reconnect = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn(`WebSocket closed...`);
|
||||
if (this.shouldReconnect) {
|
||||
this.reconnect = true;
|
||||
setTimeout(() => {
|
||||
console.log("Reconnect WebSocket...");
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
|
||||
class ScannerManager {
|
||||
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.socket = null;
|
||||
this.axes = 0;
|
||||
this.cropping = 1;
|
||||
this.debug_count = 0
|
||||
}
|
||||
|
||||
toggle_median() { this.axes = !this.axes; return this.axes; }
|
||||
toggle_crop() { this.croping = !this.croping; return this.croping; }
|
||||
|
||||
init_controls() {
|
||||
this.session= sId("_session");
|
||||
this.ts = sId("_ts");
|
||||
this.x = sId("_x");
|
||||
this.y = sId("_y");
|
||||
this.debug = sId("_debug");
|
||||
const scan = sId("_scan");
|
||||
const halt = sId("_halt");
|
||||
const median = sId("_median");
|
||||
const crop = sId("_crop");
|
||||
|
||||
median.addEventListener('click',(e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); });
|
||||
crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); });
|
||||
scan.addEventListener('click', (e) => { this.scan(); });
|
||||
halt.addEventListener('click', (e) => { this.halt(); });
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
this.socket = socket;
|
||||
this.init_controls();
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
try {
|
||||
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
|
||||
if (payload.xy) { this.x.textContent=payload.x.toFixed(2); this.y.textContent=payload.y.toFixed(2); }
|
||||
if (payload.state) { this.debug.insertAdjacentHTML('afterbegin', `<li>[ ${++this.debug_count} - ${payload.state} ]: ${payload.msg}</li>`); }
|
||||
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
init() { this.axes = 0; this.cropping = 1; this._send({ type: 'scanner', topic: "init", }); }
|
||||
scan() { this._send({ type: 'scanner', topic: "scan", session: this.session.value ? this.session.value: "0" }); }
|
||||
halt() { this._send({ type: 'calibrate', topic: "halt" }); }
|
||||
|
||||
_send(message) { this.socket.send(message); }
|
||||
}
|
||||
|
||||
class MetadataSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.manager = null;
|
||||
this.reconnectDelay = 1000;
|
||||
this.shouldReconnect = true;
|
||||
this.reconnect = false;
|
||||
}
|
||||
|
||||
setManager(manager) { this.manager = manager; }
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.manager.update(data);
|
||||
};
|
||||
|
||||
this.ws.onopen = (event) => {
|
||||
if (this.manager && !this.reconnect)
|
||||
this.manager['init']();
|
||||
this.reconnect = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn(`WebSocket closed...`);
|
||||
if (this.shouldReconnect) {
|
||||
this.reconnect = true;
|
||||
setTimeout(() => {
|
||||
console.log("Reconnect WebSocket...");
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
class DualRangeSlider {
|
||||
constructor(root, options = {}) {
|
||||
// root: element containing the slider UI
|
||||
this.root = (typeof root === 'string') ? document.querySelector(root) : root;
|
||||
if (!this.root) throw new Error('Root element not found');
|
||||
|
||||
// elements
|
||||
this.slider = this.root.querySelector('.slider');
|
||||
this.minRange = this.root.querySelector('.min-range');
|
||||
this.maxRange = this.root.querySelector('.max-range');
|
||||
this.minNumber = this.root.querySelector('.min-number');
|
||||
this.maxNumber = this.root.querySelector('.max-number');
|
||||
this.displayRange = this.root.querySelector('.display-range');
|
||||
this.highlight = this.root.querySelector('.range-highlight');
|
||||
|
||||
// config
|
||||
this.min = Number(this.slider.dataset.min ?? this.minRange.min ?? 0);
|
||||
this.max = Number(this.slider.dataset.max ?? this.minRange.max ?? 100);
|
||||
this.gap = Number(options.gap ?? 0);
|
||||
|
||||
this._bindEvents();
|
||||
this.updateFromInputs();
|
||||
}
|
||||
|
||||
// calc & repaint
|
||||
updateHighlight() {
|
||||
const a = Number(this.minRange.value);
|
||||
const b = Number(this.maxRange.value);
|
||||
const pctA = (a - this.min) / (this.max - this.min) * 100;
|
||||
const pctB = (b - this.min) / (this.max - this.min) * 100;
|
||||
this.highlight.style.left = pctA + '%';
|
||||
this.highlight.style.width = (pctB - pctA) + '%';
|
||||
}
|
||||
|
||||
// keep values coherent and sync UI
|
||||
updateFromInputs(triggerSource) {
|
||||
let a = Number(this.minRange.value);
|
||||
let b = Number(this.maxRange.value);
|
||||
|
||||
if (a > b - this.gap) {
|
||||
if (triggerSource === 'min') a = b - this.gap;
|
||||
else if (triggerSource === 'max') b = a + this.gap;
|
||||
else a = Math.min(a, b - this.gap);
|
||||
}
|
||||
|
||||
a = Math.max(this.min, Math.min(a, this.max));
|
||||
b = Math.max(this.min, Math.min(b, this.max));
|
||||
|
||||
this.minRange.value = a;
|
||||
this.maxRange.value = b;
|
||||
this.minNumber.value = a;
|
||||
this.maxNumber.value = b;
|
||||
//if (this.displayRange) this.displayRange.textContent = a + ' — ' + b;
|
||||
if (this.displayRange)
|
||||
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${a}</div><div class="w3-half w3-right-align">${b}</div></div>`;
|
||||
|
||||
this.updateHighlight();
|
||||
}
|
||||
|
||||
updateFromNumbers() {
|
||||
let a = Number(this.minNumber.value) || this.min;
|
||||
let b = Number(this.maxNumber.value) || this.max;
|
||||
if (a > b - this.gap) a = b - this.gap;
|
||||
a = Math.max(this.min, Math.min(a, this.max));
|
||||
b = Math.max(this.min, Math.min(b, this.max));
|
||||
this.minRange.value = a;
|
||||
this.maxRange.value = b;
|
||||
this.minNumber.value = a;
|
||||
this.maxNumber.value = b;
|
||||
//if (this.displayRange) this.displayRange.textContent = a + ' — ' + b;
|
||||
if (this.displayRange)
|
||||
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${a}</div><div class="w3-half w3-right-align">${b}</div></div>`;
|
||||
this.updateHighlight();
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
this.minRange.addEventListener('input', () => this.updateFromInputs('min'));
|
||||
this.maxRange.addEventListener('input', () => this.updateFromInputs('max'));
|
||||
this.minNumber.addEventListener('change', () => this.updateFromNumbers());
|
||||
this.maxNumber.addEventListener('change', () => this.updateFromNumbers());
|
||||
|
||||
// click on track reposition nearest thumb
|
||||
this.slider.addEventListener('click', (e) => {
|
||||
if (e.target.tagName.toLowerCase() === 'input') return;
|
||||
const rect = this.slider.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const pct = clickX / rect.width;
|
||||
const value = Math.round(pct * (this.max - this.min) + this.min);
|
||||
const distMin = Math.abs(value - Number(this.minRange.value));
|
||||
const distMax = Math.abs(value - Number(this.maxRange.value));
|
||||
if (distMin <= distMax) {
|
||||
this.minRange.value = Math.min(value, Number(this.maxRange.value));
|
||||
} else {
|
||||
this.maxRange.value = Math.max(value, Number(this.minRange.value));
|
||||
}
|
||||
this.updateFromInputs();
|
||||
});
|
||||
}
|
||||
|
||||
// public API: get current values
|
||||
getValues() {
|
||||
return { min: Number(this.minRange.value), max: Number(this.maxRange.value) };
|
||||
}
|
||||
|
||||
// public API: set values programmatically
|
||||
setValues(a, b) {
|
||||
this.minRange.value = a;
|
||||
this.maxRange.value = b;
|
||||
this.updateFromInputs();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Exemple d'usage
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// initialisation simple
|
||||
const slider1 = new DualRangeSlider('#myRangeSlider', { gap: 0 });
|
||||
|
||||
// accéder aux valeurs
|
||||
// console.log(slider1.getValues());
|
||||
|
||||
// modifier les valeurs depuis le code
|
||||
// slider1.setValues(10, 50);
|
||||
});
|
||||
*/
|
||||
/**
|
||||
* Crée le DOM du slider et instancie DualRangeSlider à partir d'options.
|
||||
* Retourne { instance, root } ou null si erreur.
|
||||
*
|
||||
* options = {
|
||||
* container: selector|Element, // obligatoire
|
||||
* min: number, max: number, // obligatoire (timestamps)
|
||||
* valueMin: number, valueMax: number, // optionnel
|
||||
* ms: boolean, // unité interne en ms
|
||||
* gap: number, // minimal gap (same unit as min/max)
|
||||
* classes: { root, slider, ... } // opcional CSS classes overrides
|
||||
* labels: { left, right } // opcional text
|
||||
* }
|
||||
*/
|
||||
function createAndInitDualRange(options = {}) {
|
||||
if (!options.container) throw new Error('options.container required');
|
||||
const parent = (typeof options.container === 'string') ? document.querySelector(options.container) : options.container;
|
||||
if (!parent) throw new Error('Container element not found');
|
||||
|
||||
if (typeof options.min === 'undefined' || typeof options.max === 'undefined') {
|
||||
throw new Error('options.min and options.max required');
|
||||
}
|
||||
|
||||
const cls = Object.assign({
|
||||
root: 'slider-container',
|
||||
display: 'display-range',
|
||||
rangeWrap: 'range-wrap slider',
|
||||
sliderSel: 'slider',
|
||||
track: 'track',
|
||||
highlight: 'range-highlight',
|
||||
minRange: 'min-range',
|
||||
maxRange: 'max-range',
|
||||
values: 'values',
|
||||
minNumber: 'min-number',
|
||||
maxNumber: 'max-number',
|
||||
}, options.classes || {});
|
||||
|
||||
// build root
|
||||
const root = document.createElement('div');
|
||||
root.className = cls.root;
|
||||
|
||||
// labels
|
||||
const labels = document.createElement('div');
|
||||
labels.className = cls.display;
|
||||
root.appendChild(labels);
|
||||
|
||||
// range wrap / slider
|
||||
const rangeWrap = document.createElement('div');
|
||||
rangeWrap.className = cls.rangeWrap;
|
||||
// set data-min/data-max as provided
|
||||
rangeWrap.dataset.min = String(options.min);
|
||||
rangeWrap.dataset.max = String(options.max);
|
||||
|
||||
const track = document.createElement('div'); track.className = cls.track;
|
||||
const highlight = document.createElement('div'); highlight.className = cls.highlight;
|
||||
rangeWrap.appendChild(track);
|
||||
rangeWrap.appendChild(highlight);
|
||||
|
||||
// inputs range
|
||||
const minR = document.createElement('input'); minR.type = 'range'; minR.className = cls.minRange;
|
||||
const maxR = document.createElement('input'); maxR.type = 'range'; maxR.className = cls.maxRange;
|
||||
minR.min = String(options.min); minR.max = String(options.max);
|
||||
maxR.min = String(options.min); maxR.max = String(options.max);
|
||||
minR.step = options.step ?? '1'; maxR.step = options.step ?? '1';
|
||||
minR.value = String(options.valueMin ?? options.min);
|
||||
maxR.value = String(options.valueMax ?? options.max);
|
||||
rangeWrap.appendChild(minR); rangeWrap.appendChild(maxR);
|
||||
root.appendChild(rangeWrap);
|
||||
|
||||
// values area
|
||||
const values = document.createElement('div'); values.className = "w3-row";
|
||||
const minLabel = document.createElement('div'); minLabel.className = "w3-half";
|
||||
const minStrong = document.createElement('span'); minStrong.textContent = 'Min)';
|
||||
const minNum = document.createElement('input'); minNum.type = 'number'; minNum.className = cls.minNumber;
|
||||
minNum.min = String(options.min); minNum.max = String(options.max); minNum.value = String(options.valueMin ?? options.min); minNum.step = options.step ?? '1';
|
||||
minLabel.appendChild(minStrong); minLabel.appendChild(minNum);
|
||||
|
||||
const maxLabel = document.createElement('div'); maxLabel.className = "w3-half w3-right-align";
|
||||
const maxStrong = document.createElement('span'); maxStrong.textContent = 'Max';
|
||||
const maxNum = document.createElement('input'); maxNum.type = 'number'; maxNum.className = cls.maxNumber;
|
||||
maxNum.min = String(options.min); maxNum.max = String(options.max); maxNum.value = String(options.valueMax ?? options.max);maxNum.step = options.step ?? '1';
|
||||
maxLabel.appendChild(maxStrong); maxLabel.appendChild(maxNum);
|
||||
|
||||
values.appendChild(minLabel); values.appendChild(maxLabel);
|
||||
root.appendChild(values);
|
||||
|
||||
// append to parent
|
||||
parent.appendChild(root);
|
||||
|
||||
const instOptions = { gap: options.gap ?? 0, ms: Boolean(options.ms), callback: options.callback ?? null };
|
||||
const instance = new DualRangeSlider(root, instOptions);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
|
||||
class DualRangeSlider {
|
||||
|
||||
constructor(root, options = {}) {
|
||||
this.root = (typeof root === 'string') ? document.querySelector(root) : root;
|
||||
if (!this.root) throw new Error('Root element not found');
|
||||
|
||||
this.slider = this.root.querySelector('.slider');
|
||||
this.minRange = this.root.querySelector('.min-range');
|
||||
this.maxRange = this.root.querySelector('.max-range');
|
||||
this.minNumber = this.root.querySelector('.min-number');
|
||||
this.maxNumber = this.root.querySelector('.max-number');
|
||||
this.displayRange = this.root.querySelector('.display-range');
|
||||
this.highlight = this.root.querySelector('.range-highlight');
|
||||
|
||||
this.useMilliseconds = Boolean(options.ms);
|
||||
this.callback = options.callback || null;
|
||||
|
||||
// lire min/max depuis data- ou attributs, puis normaliser en unité interne
|
||||
const rawMin = Number(this.slider.dataset.min ?? this.minRange.min ?? 0);
|
||||
const rawMax = Number(this.slider.dataset.max ?? this.minRange.max ?? 100);
|
||||
|
||||
// unité interne = ms si useMilliseconds true, sinon seconds
|
||||
this.min = this.useMilliseconds ? rawMin : rawMin;
|
||||
this.max = this.useMilliseconds ? rawMax : rawMax;
|
||||
|
||||
// Si ms=true mais HTML inputs fournis en secondes, détecter et convertir automatiquement
|
||||
// Détection simple : si ms=true et raw values semblent petits (<1e12), on multiplie par 1000
|
||||
if (this.useMilliseconds) {
|
||||
const needsConversion = (n) => n && n < 1e12; // timestamp en s
|
||||
if (needsConversion(this.min)) this.min *= 1000;
|
||||
if (needsConversion(this.max)) this.max *= 1000;
|
||||
} else {
|
||||
// si ms=false mais raw semblent en ms (>1e12), convertir en s
|
||||
const needsConversion = (n) => n && n > 1e12;
|
||||
if (needsConversion(this.min)) this.min = Math.floor(this.min / 1000);
|
||||
if (needsConversion(this.max)) this.max = Math.floor(this.max / 1000);
|
||||
}
|
||||
|
||||
// normaliser les inputs HTML pour qu'ils correspondent à l'unité interne
|
||||
const setInputRangeAttrs = (el, val) => {
|
||||
if (!el) return;
|
||||
if (this.useMilliseconds) el.min = String(this.min);
|
||||
else el.min = String(this.min);
|
||||
if (this.useMilliseconds) el.max = String(this.max);
|
||||
else el.max = String(this.max);
|
||||
// si value absent, initialiser
|
||||
if (!el.value) el.value = el.min;
|
||||
};
|
||||
|
||||
setInputRangeAttrs(this.minRange);
|
||||
setInputRangeAttrs(this.maxRange);
|
||||
if (this.minNumber) { this.minNumber.min = String(this.min); this.minNumber.max = String(this.max); }
|
||||
if (this.maxNumber) { this.maxNumber.min = String(this.min); this.maxNumber.max = String(this.max); }
|
||||
|
||||
this.gap = Number(options.gap ?? 0) * (this.useMilliseconds ? 1000 : 1);
|
||||
|
||||
// if inputs have values, ensure they are in internal unit; convert if necessary
|
||||
const normalizeInputValue = (el) => {
|
||||
if (!el) return;
|
||||
let v = Number(el.value || el.getAttribute('value') || this.min);
|
||||
if (this.useMilliseconds && v < 1e12) v = v * 1000;
|
||||
if (!this.useMilliseconds && v > 1e12) v = Math.floor(v / 1000);
|
||||
el.value = String(Math.max(this.min, Math.min(v, this.max)));
|
||||
};
|
||||
normalizeInputValue(this.minRange);
|
||||
normalizeInputValue(this.maxRange);
|
||||
normalizeInputValue(this.minNumber);
|
||||
normalizeInputValue(this.maxNumber);
|
||||
|
||||
this._bindEvents();
|
||||
this.updateFromInputs();
|
||||
}
|
||||
|
||||
_toDate(ts) {
|
||||
// ts is in internal unit: ms if useMilliseconds else seconds
|
||||
return this.useMilliseconds ? new Date(Number(ts)) : new Date(Number(ts) * 1000);
|
||||
}
|
||||
|
||||
_formatDate(ts) {
|
||||
const d = this._toDate(ts);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
updateHighlight() {
|
||||
const a = Number(this.minRange.value);
|
||||
const b = Number(this.maxRange.value);
|
||||
|
||||
if (this.callback) this.callback(a, b);
|
||||
|
||||
const pctA = (a - this.min) / (this.max - this.min) * 100;
|
||||
const pctB = (b - this.min) / (this.max - this.min) * 100;
|
||||
this.highlight.style.left = pctA + '%';
|
||||
this.highlight.style.width = (pctB - pctA) + '%';
|
||||
}
|
||||
|
||||
updateFromInputs(triggerSource) {
|
||||
let a = Number(this.minRange.value);
|
||||
let b = Number(this.maxRange.value);
|
||||
|
||||
if (a > b - this.gap) {
|
||||
if (triggerSource === 'min') a = b - this.gap;
|
||||
else if (triggerSource === 'max') b = a + this.gap;
|
||||
else a = Math.min(a, b - this.gap);
|
||||
}
|
||||
|
||||
a = Math.max(this.min, Math.min(a, this.max));
|
||||
b = Math.max(this.min, Math.min(b, this.max));
|
||||
|
||||
this.minRange.value = String(a);
|
||||
this.maxRange.value = String(b);
|
||||
if (this.minNumber) this.minNumber.value = String(a);
|
||||
if (this.maxNumber) this.maxNumber.value = String(b);
|
||||
//if (this.displayRange) this.displayRange.textContent = `${this._formatDate(a)} — ${this._formatDate(b)}`;
|
||||
if (this.displayRange)
|
||||
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${this._formatDate(a)}</div><div class="w3-half w3-right-align">${this._formatDate(b)}</div></div>`;
|
||||
this.updateHighlight();
|
||||
}
|
||||
|
||||
updateFromNumbers() {
|
||||
let a = Number(this.minNumber.value) || this.min;
|
||||
let b = Number(this.maxNumber.value) || this.max;
|
||||
if (a > b - this.gap) a = b - this.gap;
|
||||
a = Math.max(this.min, Math.min(a, this.max));
|
||||
b = Math.max(this.min, Math.min(b, this.max));
|
||||
this.minRange.value = String(a);
|
||||
this.maxRange.value = String(b);
|
||||
this.minNumber.value = String(a);
|
||||
this.maxNumber.value = String(b);
|
||||
//if (this.displayRange) this.displayRange.textContent = `${this._formatDate(a)} — ${this._formatDate(b)}`;
|
||||
if (this.displayRange)
|
||||
this.displayRange.innerHTML = `<div class="w3-row"><div class="w3-half">${this._formatDate(a)}</div><div class="w3-half w3-right-align">${this._formatDate(b)}</div></div>`;
|
||||
this.updateHighlight();
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
this.minRange.addEventListener('input', () => this.updateFromInputs('min'));
|
||||
this.maxRange.addEventListener('input', () => this.updateFromInputs('max'));
|
||||
if (this.minNumber) this.minNumber.addEventListener('change', () => this.updateFromNumbers());
|
||||
if (this.maxNumber) this.maxNumber.addEventListener('change', () => this.updateFromNumbers());
|
||||
|
||||
this.slider.addEventListener('click', (e) => {
|
||||
if (e.target.tagName.toLowerCase() === 'input') return;
|
||||
const rect = this.slider.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const pct = clickX / rect.width;
|
||||
const value = Math.round(pct * (this.max - this.min) + this.min);
|
||||
const distMin = Math.abs(value - Number(this.minRange.value));
|
||||
const distMax = Math.abs(value - Number(this.maxRange.value));
|
||||
if (distMin <= distMax) {
|
||||
this.minRange.value = String(Math.min(value, Number(this.maxRange.value)));
|
||||
} else {
|
||||
this.maxRange.value = String(Math.max(value, Number(this.minRange.value)));
|
||||
}
|
||||
this.updateFromInputs();
|
||||
});
|
||||
}
|
||||
|
||||
getValues() {
|
||||
return { min: Number(this.minRange.value), max: Number(this.maxRange.value) };
|
||||
}
|
||||
|
||||
setValues(a, b) {
|
||||
// a,b given in same unit as useMilliseconds setting
|
||||
this.minRange.value = String(a);
|
||||
this.maxRange.value = String(b);
|
||||
this.updateFromInputs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée le DOM du slider et instancie DualRangeSlider à partir d'options.
|
||||
* Retourne { instance, root } ou null si erreur.
|
||||
*
|
||||
* options = {
|
||||
* container: selector|Element, // obligatoire
|
||||
* min: number, max: number, // obligatoire (timestamps)
|
||||
* valueMin: number, valueMax: number, // optionnel
|
||||
* ms: boolean, // unité interne en ms
|
||||
* gap: number, // minimal gap (same unit as min/max)
|
||||
* classes: { root, slider, ... } // opcional CSS classes overrides
|
||||
* labels: { left, right } // opcional text
|
||||
* }
|
||||
*/
|
||||
function createAndInitDualRange(options = {}) {
|
||||
if (!options.container) throw new Error('options.container required');
|
||||
const parent = (typeof options.container === 'string') ? document.querySelector(options.container) : options.container;
|
||||
if (!parent) throw new Error('Container element not found');
|
||||
|
||||
if (typeof options.min === 'undefined' || typeof options.max === 'undefined') {
|
||||
throw new Error('options.min and options.max required');
|
||||
}
|
||||
|
||||
const cls = Object.assign({
|
||||
root: 'slider-container',
|
||||
display: 'display-range',
|
||||
rangeWrap: 'range-wrap slider',
|
||||
sliderSel: 'slider',
|
||||
track: 'track',
|
||||
highlight: 'range-highlight',
|
||||
minRange: 'min-range',
|
||||
maxRange: 'max-range',
|
||||
values: 'values',
|
||||
minNumber: 'min-number',
|
||||
maxNumber: 'max-number',
|
||||
}, options.classes || {});
|
||||
|
||||
// build root
|
||||
const root = document.createElement('div');
|
||||
root.className = cls.root;
|
||||
|
||||
// labels
|
||||
const labels = document.createElement('div');
|
||||
labels.className = cls.display;
|
||||
root.appendChild(labels);
|
||||
|
||||
// range wrap / slider
|
||||
const rangeWrap = document.createElement('div');
|
||||
rangeWrap.className = cls.rangeWrap;
|
||||
// set data-min/data-max as provided
|
||||
rangeWrap.dataset.min = String(options.min);
|
||||
rangeWrap.dataset.max = String(options.max);
|
||||
|
||||
const track = document.createElement('div'); track.className = cls.track;
|
||||
const highlight = document.createElement('div'); highlight.className = cls.highlight;
|
||||
rangeWrap.appendChild(track);
|
||||
rangeWrap.appendChild(highlight);
|
||||
|
||||
// inputs range
|
||||
const minR = document.createElement('input'); minR.type = 'range'; minR.className = cls.minRange;
|
||||
const maxR = document.createElement('input'); maxR.type = 'range'; maxR.className = cls.maxRange;
|
||||
minR.min = String(options.min); minR.max = String(options.max);
|
||||
maxR.min = String(options.min); maxR.max = String(options.max);
|
||||
minR.step = options.step ?? '1'; maxR.step = options.step ?? '1';
|
||||
minR.value = String(options.valueMin ?? options.min);
|
||||
maxR.value = String(options.valueMax ?? options.max);
|
||||
rangeWrap.appendChild(minR); rangeWrap.appendChild(maxR);
|
||||
root.appendChild(rangeWrap);
|
||||
|
||||
// values area
|
||||
const values = document.createElement('div'); values.className = "w3-row";
|
||||
const minLabel = document.createElement('div'); minLabel.className = "w3-half";
|
||||
const minStrong = document.createElement('span'); minStrong.textContent = 'Min (ms)';
|
||||
const minNum = document.createElement('input'); minNum.type = 'number'; minNum.className = cls.minNumber;
|
||||
minNum.min = String(options.min); minNum.max = String(options.max); minNum.value = String(options.valueMin ?? options.min); minNum.step = options.step ?? '1';
|
||||
minLabel.appendChild(minStrong); minLabel.appendChild(minNum);
|
||||
|
||||
const maxLabel = document.createElement('div'); maxLabel.className = "w3-half w3-right-align";
|
||||
const maxStrong = document.createElement('span'); maxStrong.textContent = 'Max (ms)';
|
||||
const maxNum = document.createElement('input'); maxNum.type = 'number'; maxNum.className = cls.maxNumber;
|
||||
maxNum.min = String(options.min); maxNum.max = String(options.max); maxNum.value = String(options.valueMax ?? options.max);maxNum.step = options.step ?? '1';
|
||||
maxLabel.appendChild(maxStrong); maxLabel.appendChild(maxNum);
|
||||
|
||||
values.appendChild(minLabel); values.appendChild(maxLabel);
|
||||
root.appendChild(values);
|
||||
|
||||
// append to parent
|
||||
parent.appendChild(root);
|
||||
|
||||
const instOptions = { gap: options.gap ?? 0, ms: Boolean(options.ms), callback: options.callback ?? null };
|
||||
const instance = new DualRangeSlider(root, instOptions);
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
|
||||
class ReplayProgressBar {
|
||||
constructor(parent, input) {
|
||||
this.input = input;
|
||||
this.parent = parent;
|
||||
this.isUserSeeking = false;
|
||||
|
||||
input.addEventListener("input", () => {
|
||||
this.isUserSeeking = true;
|
||||
const percent = input.value / 1000;
|
||||
this.parent.seek(percent);
|
||||
});
|
||||
input.addEventListener("change", () => {
|
||||
this.isUserSeeking = false;
|
||||
});
|
||||
}
|
||||
update(progress) {
|
||||
if (this.isUserSeeking) return;
|
||||
this.input.value = progress * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
class ReplaySpeedControl {
|
||||
constructor(parent, initialSpeed = 1) {
|
||||
this.parent = parent;
|
||||
|
||||
this.SPEED_STEPS = [0.25, 0.5, 0.75, 1, 2, 4, 8, 16, 32];
|
||||
const initialIndex = this.SPEED_STEPS.indexOf(initialSpeed) !== -1
|
||||
? this.SPEED_STEPS.indexOf(initialSpeed)
|
||||
: this.SPEED_STEPS.indexOf(1);
|
||||
|
||||
this.parent.speed_control.addEventListener("input", () => {
|
||||
this.parent.speed_label.textContent = this.SPEED_STEPS[this.parent.speed_control.value];
|
||||
});
|
||||
|
||||
this.parent.speed_control.addEventListener("change", () => {
|
||||
const speed = this.getSpeed();
|
||||
this.parent.setSpeed(speed);
|
||||
});
|
||||
}
|
||||
getSpeed() {
|
||||
return this.SPEED_STEPS[this.parent.speed_control.value];
|
||||
}
|
||||
updateSpeed(speed) {
|
||||
this.parent.speed_control.value = this.SPEED_STEPS.indexOf(speed);
|
||||
this.parent.speed_label.textContent = this.SPEED_STEPS[this.parent.speed_control.value];
|
||||
}
|
||||
}
|
||||
|
||||
class ReplayManager {
|
||||
|
||||
constructor(options = {}) {
|
||||
this.img = options.img;
|
||||
this.btplay = options.play;
|
||||
this.btpause= options.pause;
|
||||
this.btstop = options.stop;
|
||||
this.ts = options.ts;
|
||||
this.btsnapshot = options.snapshot;
|
||||
this.btvideosnap = options.videosnap;
|
||||
|
||||
this.speed_label = options.speed_label;
|
||||
this.speed_control = options.speed_control;
|
||||
this.ts_iso = options.ts_iso;
|
||||
this.percent = options.percent;
|
||||
this.dt_left = options.dt_left;
|
||||
this.dt_right = options.dt_right;
|
||||
this.timeline = options.timeline;
|
||||
|
||||
this.uuid = options.uuid;
|
||||
this.fps = options.fps;
|
||||
this.video_endpoint = options.video_endpoint;
|
||||
|
||||
this.dt_start = options.dt_start;
|
||||
this.dt_stop = options.dt_stop;
|
||||
this.video_type = options.video_type || 'mp4';
|
||||
this.state = null;
|
||||
this.socket = null;
|
||||
this.cursor = null;
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (!this.uuid) return;
|
||||
this.btplay.addEventListener('click', (e) => { this.play(); });
|
||||
this.btpause.addEventListener('click', (e) => { this.pause(); });
|
||||
this.btstop.addEventListener('click', (e) => { this.stop(); });
|
||||
this.btsnapshot.addEventListener('click', (e) => { this.snapshot(); });
|
||||
this.btvideosnap.addEventListener('click', (e) => { this.videosnap(); });
|
||||
|
||||
this.speedControl = new ReplaySpeedControl(this);
|
||||
this.progressBar = new ReplayProgressBar(this, this.timeline);
|
||||
|
||||
this._update_container(this.dt_start);
|
||||
this.updateState("stopped");
|
||||
}
|
||||
|
||||
updateProgessBar(progress) {
|
||||
if (this.progressBar) {
|
||||
this.progressBar.update(progress);
|
||||
}
|
||||
}
|
||||
|
||||
updateState(state) {
|
||||
const rules = {
|
||||
stopped: { play: true, pause: false, stop: false, range: true, snapshot: true, videosnap: true },
|
||||
playing: { play: false, pause: true, stop: true, range: false , snapshot: false, videosnap: false },
|
||||
paused: { play: true, pause: false, stop: true, range: true , snapshot: true, videosnap: true }
|
||||
};
|
||||
const rule = rules[state];
|
||||
this.btplay.disabled = !rule.play;
|
||||
this.btpause.disabled = !rule.pause;
|
||||
this.btstop.disabled = !rule.stop;
|
||||
this.btsnapshot.disabled = !rule.snapshot;
|
||||
this.btvideosnap.disabled = !rule.videosnap;
|
||||
this.timeline.disabled = rule.range;
|
||||
}
|
||||
|
||||
_setState(state) {
|
||||
this.state = state;
|
||||
this.updateState(state);
|
||||
}
|
||||
|
||||
_update_container(cursor) {
|
||||
this.cursor = cursor;
|
||||
this.filter = {
|
||||
uuid: this.uuid,
|
||||
dt_start: cursor,
|
||||
dt_stop: this.dt_stop,
|
||||
fps: this.fps,
|
||||
speed: this.speedControl.getSpeed()
|
||||
}
|
||||
this.percent.textContent = '';
|
||||
this.updateProgessBar(0.0);
|
||||
const dt = cursor / 1_000;
|
||||
this.ts_iso.textContent = toLocalISOString(new Date(dt));
|
||||
this.dt_left.textContent = timestampToLocalISOString(dt/1_000);
|
||||
this.dt_right.textContent = timestampToLocalISOString(this.dt_stop/1_000_000);
|
||||
this._setState("stopped");
|
||||
}
|
||||
|
||||
_update_content_slider(ts, progress) {
|
||||
this.cursor = ts;
|
||||
this.ts_iso.textContent = toLocalISOString(new Date(ts/1000));
|
||||
const percent = Math.ceil(progress * 1000000) / 10000;
|
||||
this.percent.textContent = percent.toFixed(3) +' %';
|
||||
this.updateProgessBar(progress);
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
try {
|
||||
if (payload.jpeg) { this.img.src = `data:image/jpeg;base64,${payload.jpeg}`; }
|
||||
if (payload.ts) { this._update_content_slider(payload.ts, payload.progress); }
|
||||
if (payload.motif === "video-reset") { this._update_container(payload.dt_start); }
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
init() { this._send({ type: 'replay', action: "init", }); }
|
||||
play() { if (this.state === "playing") return; this._setState("playing"); this._send({ type: 'replay', action: "play", }); }
|
||||
pause() { if (this.state !== "playing") return; this._setState("paused"); this._send({ type: 'replay', action: "pause", }); }
|
||||
stop() { this._setState("stopped"); this._send({ type: 'replay', action: "stop", }); }
|
||||
|
||||
setSpeed(speed) { this._send({ type: 'replay', action: "speed", value: speed }); }
|
||||
seek(percent) { this._send({ type: 'replay', action: "seek", value: percent }); }
|
||||
|
||||
videosnap() {
|
||||
let filename = `${this.uuid}-${this.ts_iso.textContent}.${this.video_type}`;
|
||||
filename = filename.replace(/ /g, '_');
|
||||
const ok = confirm(`Télécharger le fichier ?\n\n${filename}`);
|
||||
if (!ok) return false;
|
||||
|
||||
fetch(this.video_endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'download',
|
||||
uuid: this.uuid,
|
||||
dt_start: this.cursor,
|
||||
dt_stop: this.dt_stop,
|
||||
fps: this.fps
|
||||
})
|
||||
}).then(res => {
|
||||
if (!res.ok) throw new Error(res.status);
|
||||
return res.blob();
|
||||
}).then(blob => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}).catch(error => {
|
||||
console.error('Erreur:', error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
let filename = `${this.uuid}-${this.ts_iso.textContent}.jpg`;
|
||||
filename = filename.replace(/ /g, '_');
|
||||
const ok = confirm(`Télécharger le fichier ?\n\n${filename}`);
|
||||
if (!ok) return false;
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = this.img.src;
|
||||
a.download = filename || 'image';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
_send(message) {
|
||||
if (!this.uuid) return;
|
||||
this.socket.send({ ...message, ...this.filter });
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.manager = null;
|
||||
this.reconnectDelay = 1000;
|
||||
this.shouldReconnect = true;
|
||||
this.reconnect = false;
|
||||
}
|
||||
|
||||
setManager(manager) { this.manager = manager; }
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.manager.update(data);
|
||||
};
|
||||
|
||||
this.ws.onopen = (event) => {
|
||||
if (this.manager && !this.reconnect)
|
||||
this.manager['init']();
|
||||
this.reconnect = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn(`WebSocket closed...`);
|
||||
if (this.shouldReconnect) {
|
||||
this.reconnect = true;
|
||||
setTimeout(() => {
|
||||
console.log("Reconnect WebSocket...");
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
}
|
||||
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
|
||||
class ScannerManager {
|
||||
|
||||
constructor(container, multiwells=null) {
|
||||
this.container = container;
|
||||
this.socket = null;
|
||||
this.multiweels = multiwells;
|
||||
this.axes = 0;
|
||||
this.cropping = 0;
|
||||
}
|
||||
|
||||
toggle_median() { this.axes = !this.axes; return this.axes; }
|
||||
toggle_crop() { this.croping = !this.croping; return this.croping; }
|
||||
|
||||
init_controls() {
|
||||
const goto_0 = sId("_goto-0");
|
||||
const goto_xy = sId("_goto-xy");
|
||||
const xy_base = sId("_xy-base");
|
||||
const xy_step = sId("_xy-step");
|
||||
const up = sId("_up");
|
||||
const down = sId("_down");
|
||||
const left = sId("_left");
|
||||
const right = sId("_right");
|
||||
this.feed = sId("_feed");
|
||||
this.step = sId("_step");
|
||||
this.well = sId("_well");
|
||||
this.x = sId("_x");
|
||||
this.y = sId("_y");
|
||||
this.dx = sId("_dx");
|
||||
this.dy = sId("_dy");
|
||||
this.xbase = sId("_xbase");
|
||||
this.ybase = sId("_ybase");
|
||||
const test = sId("_test");
|
||||
const halt = sId("_halt");
|
||||
|
||||
const median = sId("_median");
|
||||
const crop = sId("_crop");
|
||||
|
||||
up.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "up" }); });
|
||||
down.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "down" }); });
|
||||
left.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "left" }); });
|
||||
right.addEventListener('mousedown', (e) => { this._send({ type: 'calibrate', topic: "right" }); });
|
||||
|
||||
goto_0.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_0" }); });
|
||||
goto_xy.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "goto_xy" }); });
|
||||
xy_base.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_base" }); });
|
||||
xy_step.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "xy_step" }); });
|
||||
|
||||
median.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "median", value: this.toggle_median() }); });
|
||||
crop.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "crop", value: this.toggle_crop() }); });
|
||||
this.well.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "well", value: e.target.value }); });
|
||||
this.step.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "step", value: e.target.value }); });
|
||||
this.feed.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "feed", value: e.target.value }); });
|
||||
|
||||
this.dx.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dx", value: e.target.value }); });
|
||||
this.dy.addEventListener("change", (e) => { this._send({ type: 'calibrate', topic: "dy", value: e.target.value }); });
|
||||
|
||||
test.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "test" }); });
|
||||
halt.addEventListener('click', (e) => { this._send({ type: 'calibrate', topic: "halt" }); });
|
||||
|
||||
}
|
||||
|
||||
registerSocket(socket) {
|
||||
this.socket = socket;
|
||||
this.init_controls();
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
try {
|
||||
if (payload.jpeg) { this.container.src = `data:image/jpeg;base64,${payload.jpeg}`; }
|
||||
if (payload.xbase) { this.xbase.textContent = payload.xbase; this.ybase.textContent = payload.ybase; }
|
||||
if (payload.dxy) { this.dy.value=payload.dy; this.dx.value=payload.dx; }
|
||||
if (payload.xy) { this.x.textContent=payload.x; this.y.textContent=payload.y; }
|
||||
//if (payload.ts) { console.log(payload.ts); }
|
||||
} catch(e) { console.log(e); }
|
||||
}
|
||||
|
||||
init() {
|
||||
this._send({
|
||||
type: 'scanner',
|
||||
topic: "init",
|
||||
feed: this.feed.value,
|
||||
step: this.step.value,
|
||||
well: this.well.value
|
||||
});
|
||||
}
|
||||
start() { this._send({ type: 'scanner', topic: "start"}); }
|
||||
halt() { this._send({ type: 'scanner', topic: "halt" }); }
|
||||
|
||||
_send(message) { this.socket.send(message); }
|
||||
}
|
||||
|
||||
class MetadataSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.ws = null;
|
||||
this.manager = null;
|
||||
this.reconnectDelay = 1000;
|
||||
this.shouldReconnect = true;
|
||||
this.reconnect = false;
|
||||
}
|
||||
|
||||
setManager(manager) { this.manager = manager; }
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.manager.update(data);
|
||||
};
|
||||
|
||||
this.ws.onopen = (event) => {
|
||||
if (this.manager && !this.reconnect)
|
||||
this.manager['init']();
|
||||
this.reconnect = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.warn(`WebSocket closed...`);
|
||||
if (this.shouldReconnect) {
|
||||
this.reconnect = true;
|
||||
setTimeout(() => {
|
||||
console.log("Reconnect WebSocket...");
|
||||
this.connect();
|
||||
}, this.reconnectDelay);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
send(obj) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(obj)); } }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
const cpu_used = sId('cpu-used');
|
||||
const shm_used = sId('shm-used');
|
||||
const mem_used = sId('mem-used');
|
||||
const disk_used = sId('disk-used');
|
||||
const ramdisk_used = sId('ramdisk-used');
|
||||
|
||||
let autoTimer = null;
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const r = await fetch(stats_endpoint, { credentials: 'same-origin' });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
const j = await r.json();
|
||||
//console.log(j);
|
||||
const cpu_percent = j.cpu_info.cpu_percent+'%'; cpu_used.style.setProperty("--cpu-used", cpu_percent); cpu_used.title=`Cpu: ${cpu_percent}`;
|
||||
const shm_length = j.shm.length; shm_used.style.setProperty("--shm_used", shm_length); shm_used.title= `Shm: ${shm_length}`;
|
||||
const virtual_memory = j.memory_info.virtual_memory.percent+'%'; mem_used.style.setProperty("--mem-used", virtual_memory); mem_used.title=`Mem: ${virtual_memory}`;
|
||||
const root_percent = j.disk_info.root.percent+'%'; disk_used.style.setProperty("--disk-used", root_percent); disk_used.title=`Disk: ${root_percent}`;
|
||||
let ramdisk_percent = "0%";
|
||||
if (! j.ramdisk_info) ramdisk_percent = j.ramdisk_info.percent+'%';
|
||||
ramdisk_used.style.setProperty("--ramdisk-used", ramdisk_percent); ramdisk_used.title=`Ramdisk: ${ramdisk_percent}`;
|
||||
|
||||
} catch (e) {
|
||||
console.log('Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
function auto_fetch_start() { fetchStats(); autoTimer = setInterval(fetchStats, 5000);}
|
||||
function auto_fetch_stop() { clearInterval(autoTimer); autoTimer = null; }
|
||||
|
||||
auto_fetch_start();
|
||||
Reference in New Issue
Block a user