Skip to content

Commit

Permalink
Release/0.0.24 (#52)
Browse files Browse the repository at this point in the history
* Display reconnection and improve auth logic
* Fix zoom dimensions

---------

Co-authored-by: micahg <[email protected]>
  • Loading branch information
micahg and micahg authored Sep 10, 2023
1 parent df2ecb9 commit 1829fec
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 143 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@micahg/ntt",
"version": "0.0.23",
"version": "0.0.24",
"files": [
"build"
],
Expand Down
280 changes: 151 additions & 129 deletions src/components/RemoteDisplayComponent/RemoteDisplayComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createRef, useEffect, useState } from 'react';
import { createRef, useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppReducerState } from '../../reducers/AppReducer';
import { loadImage, renderImageFullScreen } from '../../utils/drawing';
Expand All @@ -18,157 +18,179 @@ const RemoteDisplayComponent = () => {
const token: string | undefined = useSelector((state: AppReducerState) => state.environment.deviceCodeToken);
const [contentCtx, setContentCtx] = useState<CanvasRenderingContext2D|null>(null);
const [overlayCtx, setOverlayCtx] = useState<CanvasRenderingContext2D|null>(null);
const [connected, setConnected] = useState<boolean|undefined>();
const [tableData, setTableData] = useState<any>();

/**
* Process a websocket message with table data
* @param data table data
* @returns nothing
*/
const processWSMessage = (data: string) => {
try {
const js = JSON.parse(data);
js.tsLocal = Date.now();
setTableData(js);
} catch(e) {
console.error(`Unable to parse WS message - ${JSON.stringify(e)}: ${JSON.stringify(data)}`);
return;
}
}

useEffect(() => {
if (!contentCanvasRef.current || contentCtx != null) return;
setContentCtx(contentCanvasRef.current.getContext('2d', { alpha: false }));
}, [contentCanvasRef, contentCtx]);

useEffect(() => {
if (!overlayCanvasRef.current || overlayCtx != null) return;
setOverlayCtx(overlayCanvasRef.current.getContext('2d', { alpha: true }));
}, [overlayCanvasRef, overlayCtx]);
/**
* Render table data
*/
const processTableData = useCallback((js: any, apiUrl: string, content: CanvasRenderingContext2D, overlay: CanvasRenderingContext2D) => {
// ignore null state -- happens when server has no useful state loaded yet
if (js.state === null) return;

useEffect(() => {
if (!noauth && !authorized) {
navigate(`/device`);
if (!js.state.viewport) {
console.error('Unable to render without viewport');
return;
}
}, [navigate, authorized, noauth])
let viewport: Rect = js.state.viewport;

useEffect(() => {
if (!overlayCtx) return;
if (!contentCtx) return;
if (!authorized && !noauth) return; // the other useEffect should go redirect us to get auth
if (!token && !noauth) return;
if (!wsUrl) {
console.error('THE OTHER IMPOSSIBLE HAS HAPPENED -- WS MESSAGE WITH NO WS URL WHAT');
if (!js.state.backgroundSize) {
console.error('Unable to render without background size');
return;
}
let tableBGSize: Rect = js.state.backgroundSize;

if ('wakeLock' in navigator) {
navigator.wakeLock.request("screen").then(() => {
console.log(`Got wake lock!`);
}).catch(() => {
console.error(`Unable to get wakelock`);
});
} else {
console.log('WakeLock unavailable');
let ts: number = new Date().getTime();
let overlayUri: string | null = null;
if ('overlay' in js.state && js.state.overlay) {
overlayUri = `${apiUrl}/${js.state.overlay}?${ts}`;
}

const fullUrl = noauth ? `${wsUrl}` : `${wsUrl}?bearer=${token}`;
const ws = new WebSocket(fullUrl);
ws.onopen = (event: Event) => {
console.log(`Got open event ${JSON.stringify(event)}`);
};

ws.onclose = (event: Event) => {
console.log(`Got closed event: ${JSON.stringify(event)}`);
let backgroundUri: string | null = null;
if ('background' in js.state && js.state.background) {
backgroundUri = `${apiUrl}/${js.state.background}?${ts}`;
}

ws.onerror = function(ev: Event) {
console.error(`MICAH got error ${JSON.stringify(ev)}`);
if (!backgroundUri) {
console.error(`Unable to determine background URL`);
return;
}

ws.onmessage = (event) => {
let data = event.data;
let js = null;
try {
js = JSON.parse(data);
} catch(e) {
console.error(`Unable to parse WS message: ${JSON.stringify(data)}`);
return;
/**
* I hate this so much... if someone ever does contribute to this
* project and your js game is better than mine, see if you can make this
* less isane. The point is to calculate the expanded the selection to
* fill the screen (based on the aspect ratio of the map) then draw the
* overlay, then the background. If there is no overlay then just draw
* background with expanded selection if there is one.
*/
loadImage(backgroundUri).then(bgImg => {
const bgVP = fillToAspect(viewport, tableBGSize, bgImg.width, bgImg.height);
if (overlayUri) {
loadImage(overlayUri).then(ovrImg => {
/* REALLY IMPORTANT - base overlay on the BG Viewport as it can shift the
* image. If the zoomed selection is so small that we render negative space
* (eg beyond the bordres) the viewport shifts to render from the border */

// start assuming no rotation (the easy case)

// TODO detect portrait - ALL OF THIS CODE assumes editor/overlay are landsacpe
let [x, y, w, h] = [0, 0, 0, 0]
if (bgImg.width < bgImg.height) {
[x, y] = rotate(90, bgVP.x, bgVP.y, bgImg.width,
bgImg.height);
let [x2, y2] = rotate(90, bgVP.x + bgVP.width, bgVP.y + bgVP.height,
bgImg.width, bgImg.height);
[x, x2] = [Math.min(x, x2), Math.max(x, x2)];
[y, y2] = [Math.min(y, y2), Math.max(y, y2)];
w = x2 - x;
h = y2 - y;
let scale = ovrImg.width/bgImg.height;
x *= scale;
y *= scale;
w *= scale;
h *= scale;
} else {
let scale = bgImg.width/ovrImg.width;
x = bgVP.x / scale;
y = bgVP.y / scale;
w = bgVP.width / scale;
h = bgVP.height / scale;
}
let olVP = {x: x, y: y, width: w, height: h};
renderImageFullScreen(ovrImg, overlay, olVP)
.then(() => renderImageFullScreen(bgImg, content, bgVP))
.catch(err => console.error(`Error rendering background or overlay image: ${JSON.stringify(err)}`));
}).catch(err => console.error(`Error loading overlay iamge ${overlayUri}: ${JSON.stringify(err)}`));
} else {
renderImageFullScreen(bgImg, content, bgVP)
.catch(err => console.error(`Error rendering background imager: ${JSON.stringify(err)}`));
}
}).catch(err => console.error(`Error loading background image: ${JSON.stringify(err)}`));
}, []);

// ignore null state -- happens when server has no useful state loaded yet
if (js.state === null) return;
useEffect(() => {
if (!contentCanvasRef.current || contentCtx != null) return;
setContentCtx(contentCanvasRef.current.getContext('2d', { alpha: false }));
}, [contentCanvasRef, contentCtx]);

// if we don't have an API URL we'll never get WS messages... seems impossible
if (apiUrl === null) {
console.error('THE IMPOSSIBLE HAS HAPPENED -- WS MESSAGE WITH NO API SERVER WHAT');
return;
}
useEffect(() => {
if (!overlayCanvasRef.current || overlayCtx != null) return;
setOverlayCtx(overlayCanvasRef.current.getContext('2d', { alpha: true }));
}, [overlayCanvasRef, overlayCtx]);

if (!js.state.viewport) {
console.error('Unable to render without viewport');
return;
}
let viewport: Rect = js.state.viewport;
/**
* First settle our authentication situation. Either confirm we are running
* without authentication or go get authenticated. When this settles, we'll
* trigger connection with the server.
*/
useEffect(() => {
// don't punt until we have successfully hit the server
if (authorized === undefined) return navigate(`/connectionerror`);

if (!js.state.backgroundSize) {
console.error('Unable to render without background size');
return;
}
let bgSize: Rect = js.state.backgroundSize;
// if authorization is ON and we are not authorized, redirect
if (!noauth && !authorized) return navigate(`/device`);

let ts: number = new Date().getTime();
let overlayUri: string | null = null;
if ('overlay' in js.state && js.state.overlay) {
overlayUri = `${apiUrl}/${js.state.overlay}?${ts}`;
}
// having authorized for the first time, start the connection loop
setConnected(false);
}, [authorized, noauth, navigate])

let backgroundUri: string | null = null;
if ('background' in js.state && js.state.background) {
backgroundUri = `${apiUrl}/${js.state.background}?${ts}`;
/**
* When authentication is sorted, figure out connectivity
*/
useEffect(() => {
if (connected === undefined) return;
if (connected) return;

let timer: NodeJS.Timer;

const scheduleConnection = (cause: string) => {
console.log(`Connection ${cause}`);
if (timer === undefined) {
setConnected(undefined);
console.log(`Setting retry timer (${cause})...`);
timer = setInterval(() => setConnected(false), 1000);
console.log(`Timer set to ${timer}`);
}
}

if (!backgroundUri) {
console.error(`Unable to determine background URL`);
return;
}
const fullUrl = noauth ? `${wsUrl}` : `${wsUrl}?bearer=${token}`;
let ws = new WebSocket(fullUrl);
console.log('Attempting connection');
ws.onclose = (event: Event) => scheduleConnection('closed');
ws.onerror = (event: Event) => scheduleConnection('error');
ws.onmessage = (event: MessageEvent) => processWSMessage(event.data);
ws.onopen = (event: Event) => console.log('Connection established');
}, [apiUrl, contentCtx, overlayCtx, connected, noauth, token, wsUrl])

/**
* With all necessary components and some table data, trigger drawing
*/
useEffect(() => {
if (!overlayCtx) return;
if (!contentCtx) return;
if (!apiUrl) return;
if (!tableData) return;

/**
* I hate this so much... if someone ever does contribute to this
* project and your js game is better than mine, see if you can make this
* less isane. The point is to calculate the expanded the selection to
* fill the screen (based on the aspect ratio of the map) then draw the
* overlay, then the background. If there is no overlay then just draw
* background with expanded selection if there is one.
*/
loadImage(backgroundUri).then(bgImg => {
let bgVP = fillToAspect(viewport, bgSize, bgImg.width, bgImg.height);
if (overlayUri) {
loadImage(overlayUri).then(ovrImg => {
/* REALLY IMPORTANT - base overlay on the BG Viewport as it can shift the
* image. If the zoomed selection is so small that we render negative space
* (eg beyond the bordres) the viewport shifts to render from the border */

// start assuming no rotation (the easy case)

// TODO detect portrait - ALL OF THIS CODE assumes editor/overlay are landsacpe
let [x, y, w, h] = [0, 0, 0, 0]
if (bgVP.width < bgVP.height) {
[x, y] = rotate(90, bgVP.x, bgVP.y, bgImg.width,
bgImg.height);
let [x2, y2] = rotate(90, bgVP.x + bgVP.width, bgVP.y + bgVP.height,
bgImg.width, bgImg.height);
[x, x2] = [Math.min(x, x2), Math.max(x, x2)];
[y, y2] = [Math.min(y, y2), Math.max(y, y2)];
w = x2 - x;
h = y2 - y;
let scale = ovrImg.width/bgImg.height;
x *= scale;
y *= scale;
w *= scale;
h *= scale;
} else {
let scale = bgImg.width/ovrImg.width;
x = bgVP.x / scale;
y = bgVP.y / scale;
w = bgVP.width / scale;
h = bgVP.height / scale;
}
let olVP = {x: x, y: y, width: w, height: h};
renderImageFullScreen(ovrImg, overlayCtx, olVP)
.then(() => renderImageFullScreen(bgImg, contentCtx, bgVP))
.catch(err => console.error(`Error rendering background or overlay image: ${JSON.stringify(err)}`));
}).catch(err => console.error(`Error loading overlay iamge ${overlayUri}: ${JSON.stringify(err)}`));
} else {
renderImageFullScreen(bgImg, contentCtx, bgVP)
.catch(err => console.error(`Error rendering background imager: ${JSON.stringify(err)}`));
}
}).catch(err => console.error(`Error loading background image: ${JSON.stringify(err)}`))
}
}, [apiUrl, wsUrl, authorized, noauth, token, contentCtx, overlayCtx]);
processTableData(tableData, apiUrl, contentCtx, overlayCtx);

}, [contentCtx, overlayCtx, apiUrl, tableData, processTableData]);

return (
<div className={styles.map}>
Expand Down
2 changes: 1 addition & 1 deletion src/reducers/EnvironmentReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const EnvironmentReducer = (state = initialState, action: PayloadAction)
case 'environment/authconfig': {
if (action.payload === null || action.payload === undefined) return state;
const authState: AuthState = (action.payload as unknown) as AuthState;
return {...state, client: authState.client, noauth: authState.noauth};
return {...state, client: authState.client, auth: authState.auth, noauth: authState.noauth};
}
case 'environment/authenticate': {
if (action.payload === null || action.payload === undefined) return state;
Expand Down
3 changes: 1 addition & 2 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function getAuthConfig(store: MiddlewareAPI<Dispatch<AnyAction>>): Promis
return new Promise((resolve, reject) => {

// ensure we have an authorization state
const auth = store.getState().auth;
const auth = store.getState().auth;
if (auth !== undefined) return resolve(auth);

// get the client auth.json and hte server noauth setting. If the server is
Expand All @@ -35,7 +35,6 @@ export function getAuthConfig(store: MiddlewareAPI<Dispatch<AnyAction>>): Promis
Promise.all([axios.get("/auth.json"), axios.get(noauthUrl)]).then(([auth, noauth]) => {
// combine the auth config into a single state
const data = auth.data;
// if (noauth.data.noauth) return reject("noauth");
data.noauth = noauth.data.noauth;
data.auth = false;
return resolve(data);
Expand Down
4 changes: 3 additions & 1 deletion src/utils/drawing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ function renderImage(image: HTMLImageElement, ctx: CanvasRenderingContext2D,

if (!ctx) return Promise.reject(`Unable to get canvas context`);

let bounds = calculateBounds(ctx.canvas.width, ctx.canvas.height, image.width, image.height);
// if we're zoomed we should use viewport width and height (not image)
const [width, height] = viewport ? [viewport.width, viewport.height] : [image.width, image.height];
const bounds = calculateBounds(ctx.canvas.width, ctx.canvas.height, width, height);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
Expand Down
Loading

0 comments on commit 1829fec

Please sign in to comment.