let client : Client; enum SequenceStatus { IDLE, RUNNING, PAUSED } class Display { private parentElement : HTMLUListElement; private canvas : HTMLCanvasElement; private ctx : CanvasRenderingContext2D; private img : HTMLImageElement; private screen : any; private sequence : boolean = false; private focus : boolean = false; private framing : boolean = false; private offsetX : number = 0; private offsetY : number = 0; private width : number = 0; private height : number = 0; private canvasScale : number = 0; private canvasWidth : number = 0; private canvasHeight : number = 0; private canvasOffsetX : number = 0; private canvasOffsetY : number = 0; private screenWidth : number = 0; private screenHeight : number = 0; private screenOffsetX : number = 0; private screenOffsetY : number = 0; private displayWidth : number = 0; private displayHeight : number = 0; private displayOffsetX : number = 0; private displayOffsetY : number = 0; constructor () { this.parentElement = document.getElementById('display') as HTMLUListElement; this.create(); window.onresize = this.onResize.bind(this); } private create () { this.canvas = this.parentElement.getElementsByTagName('canvas')[0]; this.ctx = this.canvas.getContext('2d'); this.screen = { width : parseInt((document.getElementById('width') as HTMLInputElement).value), height : parseInt((document.getElementById('height') as HTMLInputElement).value) } this.updateSize(); this.clear(); this.updateScreen(); this.updateDisplay(); } private updateSize () { this.canvasScale = window.devicePixelRatio; this.canvasWidth = this.parentElement.clientWidth - 12; this.canvasHeight = this.parentElement.clientHeight - 12; //console.log(`${this.canvasWidth},${this.canvasHeight}`); this.canvas.width = this.canvasWidth; this.canvas.height = this.canvasHeight; } public clear () { this.ctx.fillStyle = 'rgb(0, 0, 0)'; this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); } public updateScreen () { const clientRatio : number = this.canvasWidth / this.canvasHeight; const screenRatio : number = this.screen.width / this.screen.height; if (screenRatio > clientRatio) { this.screenWidth = this.canvasWidth - 2; this.screenHeight = Math.floor(this.canvasWidth / screenRatio); this.screenOffsetX = 1; this.screenOffsetY = Math.round((this.canvasHeight - this.screenHeight) / 2); } else { this.screenWidth = Math.round(this.canvasHeight * screenRatio); this.screenHeight = this.canvasHeight - 2; this.screenOffsetY = 1; this.screenOffsetX = Math.round((this.canvasWidth - this.screenWidth) / 2); } this.ctx.strokeStyle = 'rgb(0, 0, 255)'; this.ctx.rect(this.screenOffsetX, this.screenOffsetY, this.screenWidth, this.screenHeight); this.ctx.stroke(); } public updateDisplay () { if (!this.sequence && !this.focus && !this.framing) { return; } //console.log(this.sequence); const screenScaleX : number = this.screenWidth / this.screen.width; const screenScaleY : number = this.screenHeight / this.screen.height; this.displayWidth = Math.round(this.width * screenScaleX); this.displayHeight = Math.round(this.height * screenScaleY); this.displayOffsetX = this.screenOffsetX + Math.round(this.offsetX * screenScaleX); this.displayOffsetY = this.screenOffsetY + Math.round(this.offsetY * screenScaleY); this.ctx.fillStyle = 'rgb(0, 0, 0)'; this.ctx.fillRect(this.displayOffsetX, this.displayOffsetY, this.displayWidth, this.displayHeight); //console.log(`${this.displayOffsetX}, ${this.displayOffsetY}, ${this.displayWidth}, ${this.displayHeight}`); this.updateImage(); } public updateImage() { this.img = new Image; this.img.onload = function () { this.ctx.drawImage(this.img, this.displayOffsetX, this.displayOffsetY, this.displayWidth, this.displayHeight); }.bind(this); this.img.src = `/${this.displayWidth}/${this.displayHeight}/image.jpg?cacheBreaker=${+new Date()}`; } public update (msg : Message) { } public set (state : State) { this.sequence = true; this.focus = false; this.offsetX = state.offset.x; this.offsetY = state.offset.y; this.width = state.display.width; this.height = state.display.height; this.updateDisplay(); } public setFocus () { this.focus = true; } public unsetFocus () { this.focus = false; } public setFraming () { this.framing= true; } public unsetFraming () { this.framing = false; } private onResize (event : any) { this.updateSize(); this.clear(); this.updateScreen(); this.updateDisplay(); } } class Client { private display : Display; private client : WebSocket; private connected : boolean = false; private progress : HTMLProgressElement; private progressText : HTMLElement; private frames : number = 0; constructor () { let uri : string = this.getWebsocketUri(); this.progress = document.getElementById('progress') as HTMLProgressElement; this.progressText = document.getElementById('progressText'); this.client = new WebSocket(uri); this.display = new Display(); this.client.onopen = this.onOpen.bind(this); this.client.onclose = this.onClose.bind(this); this.client.onmessage = this.onMessage.bind(this); this.resetForm('sequenceSelectForm'); this.resetForm('sequenceCtrlForm'); this.resetForm('manualCtrlForm'); this.resetForm('exposureCtrlForm'); this.resetForm('statisticsForm'); this.disableClass('sequenceCtrl'); this.disableClass('manualCtrl'); this.disableClass('exposureCtrl'); this.setProgress({ hash: null, progress: 0 }); } private getWebsocketUri () : string { const host : string = (window.location.host + '').split(':')[0]; //WEBSOCKET_PORT defined on page via template //@ts-ignore return `ws://${host}:${WEBSOCKET_PORT}` } private onMessage (event : any) { const msg : Message = JSON.parse(event.data) as Message; if (typeof msg.cmd !== 'undefined' && msg.cmd !== null) { this.cmd(msg); } } private onOpen (event : any) { console.log('Connected'); this.connected = true; this.active(); this.enableClass('manualCtrl'); } private onClose (event : any) { console.log('Disconnected'); this.connected = false; this.inactive(); } private setSequence(state : State) { this.setProgress(state.sequence); this.setFrame(state.sequence); this.setStatus(state.sequence); this.setExposure(state); this.setDisplay(state); this.set('sequence', state.sequence.hash); this.removeClass('sequence', 'edited'); } private setUpdate(state : State) { this.setProgress(state.sequence); this.setFrame(state.sequence); this.setStatus(state.sequence); this.setExposure(state); this.setStatistics(state.statistics); this.display.updateImage(); } private setStatus (sequence : SequenceState) { let status : string; switch (sequence.status) { case SequenceStatus.IDLE : status = 'Idle'; break; case SequenceStatus.RUNNING : status = 'Running'; break; case SequenceStatus.PAUSED : status = 'Paused'; break; default : status = 'Unknown State'; } this.frames = sequence.frames; document.getElementById('sequenceStatus').innerText = status; document.getElementById('sequenceName').innerText = sequence.name; document.getElementById('sequenceLength').innerText = `Sequence Length: ${sequence.frames}`; } private setProgress (sequence : SequenceState) { const percent : number = sequence.progress * 100.0; this.progress.value = sequence.progress; this.progressText.innerText = `Progress: ${Math.floor(percent)}%`; } private setFrame (sequence : SequenceState) { if (typeof sequence.current !== 'undefined') { this.set('frame', `${sequence.current}`.padStart(5, '0')); this.removeClass('frame', 'edited'); } } private setExposure (state : State) { if (typeof state.exposure !== 'undefined') { const el : HTMLInputElement = document.getElementById('exposure') as HTMLInputElement; this.enableClass('exposureCtrl'); this.set('exposure', `${state.exposure}`); this.removeClass('exposure', 'edited'); } } private setDisplay (state : State) { if (typeof state.display !== 'undefined') { this.set('displayWidth', state.display.width.toString()); this.set('displayHeight', state.display.height.toString()); this.set('offsetLeft', state.offset.x.toString()); this.set('offsetTop', state.offset.y.toString()); this.set('sourceWidth', state.source.width.toString()); this.set('sourceHeight', state.source.height.toString()); //widthEl.readOnly = false; //heightEl.readOnly = false; //console.dir(state); this.display.set(state); } } public edited (el : HTMLElement) { el.classList.add('edited'); } private setStatistics (stats : SequenceStatistics) { if (stats !== null) { this.set('statsFrameTotalLast', this.roundDigits(stats.totalFrameLast, 0)); this.set('statsFrameTotalAvg', this.roundDigits(stats.totalFrameAvg, 2)); this.set('statsFrameTotalMargin', this.roundDigits(stats.totalFrameMargin, 1)); this.set('statsFPS', this.roundDigits(stats.fps, 2)); this.set('statsFrameLoadAvg', this.roundDigits(stats.loadAvg, 2)); this.set('statsFrameLoadMargin', this.roundDigits(stats.loadMargin, 1)); this.set('statsFrameOpenLast', this.roundDigits(stats.openLast, 0)); this.set('statsFrameOpenAvg', this.roundDigits(stats.openAvg, 2)); this.set('statsFrameOpenMargin', this.roundDigits(stats.openMargin, 1)); this.set('statsFrameCloseLast', this.roundDigits(stats.closeLast, 0)); this.set('statsFrameCloseAvg', this.roundDigits(stats.closeAvg, 2)); this.set('statsFrameCloseMargin', this.roundDigits(stats.closeMargin, 1)); this.set('statsExposureLast', this.roundDigits(stats.exposureLast, 0)); this.set('statsExposureAvg', this.roundDigits(stats.exposureAvg, 2)); this.set('statsExposureMargin', this.roundDigits(stats.exposureMargin, 1)); this.set('statsElapsed', this.roundDigits(stats.elapsed, 0)); this.set('statsEstimate', this.roundDigits(stats.estimate, 0)); this.set('statsElapsedHuman', this.shortenHumanize(Math.round(stats.elapsed))); this.set('statsEstimateHuman', this.shortenHumanize(Math.round(stats.estimate))); } } private cmd (msg : Message) { switch (msg.cmd) { case 'ping' : this.receivePing(); break; case 'open' : this.receiveCameraOpen(); break; case 'close' : this.receiveCameraClose(); break; case 'select' : this.receiveSelect(msg); break; case 'update' : this.receiveUpdate(msg); break; case 'focus' : this.receiveFocus(msg); break; case 'unfocus' : this.receiveUnfocus(msg); break; case 'framing' : this.receiveFraming(msg); break; case 'unframing' : this.receiveUnframing(msg); break; case 'display' : this.receiveDisplay(msg); break; default: console.warn(`No command "${msg.cmd}"`); break; } } public disableClass (className : string) { console.log(`Disabling class: ${className}`); document.querySelectorAll(`.${className}`).forEach((el : HTMLButtonElement) => { el.disabled = true; }); } public enableClass (className : string) { console.log(`Enabling class: ${className}`); document.querySelectorAll(`.${className}`).forEach((el : HTMLButtonElement) => { el.disabled = false; }); } public sendCameraOpen () { console.log('send camera open'); this.disableClass('manualCtrl'); this.client.send(JSON.stringify({ cmd : 'open' })); } private receivePing() { this.sendPong(); } private receiveCameraOpen () { console.log('got camera open'); this.enableClass('manualCtrl'); } public sendCameraClose () { console.log('send camera close'); this.disableClass('manualCtrl'); this.client.send(JSON.stringify({ cmd : 'close' })); } private receiveCameraClose () { console.log('got camera close'); this.enableClass('manualCtrl'); } private sendPong () { this.client.send(JSON.stringify({ cmd : 'pong' })); } public sendAdvance () { this.client.send(JSON.stringify({ cmd : 'advance' })); } public sendRewind () { this.client.send(JSON.stringify({ cmd : 'rewind' })); } public sendFrameSet () { const frameStr : string = (document.getElementById('frame') as HTMLInputElement).value; let frameNum : number = null; try { frameNum = parseInt(frameStr); } catch (err) { console.error(`Error parsing ${frameStr}`); } if (frameNum === null) { frameNum = 0; } if (frameNum < 0) { frameNum = 0; } if (frameNum > this.frames - 1) { frameNum = this.frames - 1; } this.client.send(JSON.stringify({ cmd : 'set', state : { sequence : { current : frameNum } } })); } public sendToEnd () { this.client.send(JSON.stringify({ cmd : 'set', state : { sequence : { current : this.frames - 1 } } })); } public sendToStart () { this.client.send(JSON.stringify({ cmd : 'set', state : { sequence : { current : 0 } } })); } public sendSelect () { const hash : string = (document.getElementById('sequence') as HTMLSelectElement ).value; let msg : Message; if (hash === '- Select Image Sequence -') { return; } msg = { cmd : 'select', state : { sequence : { hash } } }; console.log(`send select ${hash}`); this.client.send(JSON.stringify(msg)); } private receiveSelect (msg : Message) { this.enableClass('sequenceCtrl'); this.setSequence(msg.state); } public sendStart () { this.client.send(JSON.stringify({ cmd : 'start' })); } public sendStop () { this.client.send(JSON.stringify({ cmd : 'stop' })); } public sendExposure () { const exposure : number = parseInt(this.get('exposure')); this.client.send(JSON.stringify({ cmd : 'exposure', state : { exposure }})); } private receiveUpdate (msg : Message) { this.setUpdate(msg.state); } private receiveDisplay (msg: Message) { this.display.clear(); this.display.updateScreen(); this.setDisplay(msg.state); } public sendFocus () { console.log('send focus'); //this.disableClass('manualCtrl'); this.client.send(JSON.stringify({ cmd : 'focus' })); } private receiveFocus (msg : Message) { this.display.setFocus(); this.display.updateImage(); } private receiveUnfocus (msg : Message) { this.display.unsetFocus(); this.display.updateImage(); } public sendFraming () { console.log('send framing'); //this.disableClass('manualCtrl'); this.client.send(JSON.stringify({ cmd : 'framing' })); } private receiveFraming (msg : Message) { this.display.setFraming(); this.display.updateImage(); } private receiveUnframing (msg : Message) { this.display.unsetFraming(); this.display.updateImage(); } public sendOffset (x : number, y : number) { this.client.send(JSON.stringify({ cmd : 'offset', x, y })); } public sendSize (width : number, height : number) { this.client.send(JSON.stringify({ cmd : 'size', width, height })); } public sendScale (scale : number) { this.client.send(JSON.stringify({ cmd : 'scale', scale })); } /** * HELPERS **/ public fullscreen () { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); } else { this.exitFullscreen(); } } public exitFullscreen () { if (document.fullscreenElement) { document.exitFullscreen() } } private active () { this.addClass('overlay', 'active'); } private inactive () { this.removeClass('overlay', 'active'); } public addClass (id : string, className : string) { document.getElementById(id).classList.add(className); } public removeClass (id : string, className : string) { document.getElementById(id).classList.remove(className); } public set (id : string, value : string) { try { (document.getElementById(id) as HTMLInputElement).value = value; } catch (err) { console.warn(`Element ${id} does not exist or cannot be set`); } } public get (id : string) { return (document.getElementById(id) as HTMLInputElement).value; } private resetForm (id : string) { (document.getElementById(id) as HTMLFormElement ).reset(); } private shortenHumanize (val : number) : string { const str : string = humanizeDuration(val, { round : true }); return str.replace('years', 'y').replace('year', 'y') .replace('months', 'mo').replace('month', 'mo') .replace('weeks', 'w').replace('week', 'w') .replace('days', 'd').replace('day', 'd') .replace('hours', 'h').replace('hour', 'h') .replace('minutes', 'm').replace('minute', 'm') .replace('seconds', 's').replace('second', 's'); } private roundDigits (val : number, digits : number) : string { const mult : number = Math.pow(10.0, digits); const rounded : number = Math.round(val * mult) / mult; return rounded.toString(); } } client = new Client();