import * as wasm from "./wasm/nexrad_browser.js"; await wasm.default(); wasm.__nxrd_browser_init(); console.log("[JS] setup event listeners"); console.log("[JS] initializing the renderer"); const DEFAULT_PREFERENCES = { RR: 5, RREN: true, FCS: 20 }; let preferences = DEFAULT_PREFERENCES; function get_font_size() { return `${preferences.FCS}px monospace`; } const canvas = document.getElementById("canvas"); function setupCanvas(canvas) { // Get the device pixel ratio, falling back to 1. var dpr = window.devicePixelRatio || 1; // Get the size of the canvas in CSS pixels. var rect = canvas.getBoundingClientRect(); // Give the canvas pixel dimensions of their CSS // size * the device pixel ratio. canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; var ctx = canvas.getContext('2d'); // Scale all drawing operations by the dpr, so you // don't have to worry about the difference. ctx.scale(dpr, dpr); return ctx; } function rescaleCanvas(canvas, ctx) { var dpr = window.devicePixelRatio || 1; // Get the size of the canvas in CSS pixels. var rect = canvas.getBoundingClientRect(); // Give the canvas pixel dimensions of their CSS // size * the device pixel ratio. canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; // Scale all drawing operations by the dpr, so you // don't have to worry about the difference. ctx.scale(dpr, dpr); } const ctx = setupCanvas(canvas); const FIFTY_MILES = 0.0916; let current_lat = 38.8977; let current_long = -77.036560; function calcRenderbox() { return [current_long - FIFTY_MILES, current_lat - FIFTY_MILES, current_long + FIFTY_MILES, current_lat + FIFTY_MILES]; } function latlongXY(lat, long) { let bbox = calcRenderbox(); let pixelWidth = canvas.width; let pixelHeight = canvas.height; let bboxWidth = bbox[2] - bbox[0]; let bboxHeight = bbox[3] - bbox[1]; let widthPct = ( long - bbox[0] ) / bboxWidth; let heightPct = ( lat - bbox[1] ) / bboxHeight; let x = Math.floor( pixelWidth * widthPct ); let y = Math.floor( pixelHeight * ( 1 - heightPct ) ); return { x, y }; } let x0 = 0; let y0 = 0; let xfull = canvas.width; let yfull = canvas.height; function recalcBorderCoordinates() { let xy = latlongXY(current_lat, current_long); x0 = xy.x - canvas.width; y0 = xy.y - canvas.height; xfull = x0 + canvas.width; yfull = y0 + canvas.width; } const red = "#ef0000"; const green = "#4af626"; const white = "#dedede"; let blinkyColor = "#dedede"; setInterval(() => { if (blinkyColor === red) { blinkyColor = white; } else { blinkyColor = red; } }, 350); function zulu() { let date = new Date(); return `${date.getUTCHours().toString().padStart(2, '0')}:${date.getUTCMinutes().toString().padStart(2, '0')}:${date.getUTCSeconds().toString().padStart(2, '0')}Z`; } function vcp(vc) { if (vc === 31) { return "CLEAR AIR MODE LONG PULSE"; } else if (vc === 35) { return "CLEAR AIR MODE"; } else if (vc === 12) { return "PRECIP MODE"; } else if (vc === 112) { return "PRECIP MODE SZ-2 PRF"; } else if (vc === 212) { return "PRECIP MODE SZ-2"; } else if (vc === 215) { return "PRECIP MODE VERT"; } } let command_buf = ""; let buf_response_mode = false; let radar_inoperative = true; let site_string = "SITE INFORMATION UNAVAILABLE"; let icao = "INOP"; let selected_mode = "INOP"; let delay_string = "DELAY UNAVAILABLE"; // dataserver status: // 0. disconnected // 1. disconnected, one-off // 2. track initializing // 3. tracked // 4. track lost let data_status = 0; function statusString() { if (data_status === 0) { return "DISCONNECTED"; } else if (data_status === 1) { return "ONEOFF"; } else if (data_status === 2) { return "TRACK INITIALIZING"; } else if (data_status === 3) { return "TRACKING"; } else if (data_status === 4) { return "TRACK LOST"; } } let display_buf = []; function recalcDisplayBuf() { display_buf = []; let line = 0; for (let i = 0; i < command_buf.length; i++) { let char = command_buf[i]; if (char === "\n") { line += 1; continue; } if (display_buf.length < line+1) { display_buf[line] = ""; } display_buf[line] += char; } } function cmd_err(err) { buf_response_mode = true; command_buf = err; recalcDisplayBuf(); } let ar2 = undefined; let new_file_available = false; document.getElementById("file").onchange = () => { new_file_available = true; } async function load() { new_file_available = false; const file = document.getElementById("file").files[0]; document.getElementById("file").value = null; const reader = new FileReader(); reader.addEventListener('load', (event) => { let data = event.target.result; let loaded = wasm.load_ar2(data); console.log(loaded); cmd_err(""); ar2 = loaded; data_status = 1; }); reader.readAsArrayBuffer(file); } function exec(command) { console.log("exec1!"); let tokens = command.split(" "); if (tokens[0] === "MODE" && tokens[1] === "SET") { let mode = tokens[2]; let valid_modes = ["REF", "VEL", "SW", "ZDR", "PHI", "RHO", "CFP"]; if (!valid_modes.includes(mode)) { cmd_err("INVALID MODE"); return; } else if (radar_inoperative) { cmd_err("RADAR INOPERATIVE\nCANNOT SET MODE"); return; } else { selected_mode = mode; return; } } if (tokens[0] === "CLF" && tokens[1] === "OV") { document.getElementById("file").click(); return; } if (tokens[0] === "CLF" && tokens[1] === "RELOAD") { // TRIGGER RELOAD cmd_err("SYSTEM PROCESSING"); load(); return; } if (command === "TEST UNSET INOP") { radar_inoperative = false; icao = "TEST"; site_string = "TEST VCP 000 TEST AIR MODE"; selected_mode = "REF"; return; } if (command === "TEST INVALID COMMAND") { cmd_err("TEST SUCCESSFUL"); return; } cmd_err("UNRECOGNIZED COMMAND"); } function shouldNewline() { if (command_buf.startsWith("MODE SET")) { return true; } return false; } let ar2_date = undefined; function convertDate() { if (ar2 !== undefined) { let unix_timestamp_millis = 86400000 * ar2.volume_header_record.date + ar2.volume_header_record.time; ar2_date = new Date(unix_timestamp_millis); } else { return undefined; } } function reRender() { if (ar2 !== undefined) { radar_inoperative = false; icao = ar2.volume_header_record.icao; site_string = `${icao} VCP ${ar2.meta_rda_status_data.vcp} ${vcp(ar2.meta_rda_status_data.vcp)}`; selected_mode = "REF"; } else { radar_inoperative = true; icao = "INOP"; site_string = "SITE INFORMATION UNAVAILABLE"; selected_mode = "INOP"; } convertDate(); if (ar2_date !== undefined) { const delay = Date.now() - ar2_date; const SEC = 1000, MIN = 60 * SEC, HRS = 60 * MIN; const humanDiff = `${Math.floor(delay/HRS)}:${Math.floor((delay%HRS)/MIN).toLocaleString('en-US', {minimumIntegerDigits: 2})}:${Math.floor((delay%MIN)/SEC).toLocaleString('en-US', {minimumIntegerDigits: 2})}`; delay_string = "SITE DELAY " + humanDiff; } //document.getElementById("input-detection").focus(); rescaleCanvas(canvas, ctx); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.resetTransform(); recalcBorderCoordinates(); let xy = latlongXY(current_lat, current_long); ctx.translate(xy.x, xy.y); // background (black, always) ctx.fillStyle = "black"; ctx.fillRect(x0, y0, xfull * 2, yfull * 2); ctx.font = get_font_size(); ctx.fillStyle = green; ctx.fillText(`NEXRAD ${icao} ${zulu()}`, x0 + 50, y0 + 50); if (radar_inoperative) { ctx.fillStyle = blinkyColor; ctx.fillText("RADR INOP NO DATA LOADED", x0 + 50, y0 + 50 + preferences.FCS); ctx.fillStyle = green; } if (new_file_available) { ctx.fillStyle = blinkyColor; ctx.fillText("NEW DATA AVAIL RLD RQD", x0 + 50, y0 + 50 + preferences.FCS * 2); ctx.fillStyle = green; } ctx.textAlign = "right"; ctx.fillText("RADAR SITE", xfull - 75, y0 + 50); ctx.fillText(`${site_string}`, xfull - 75, y0 + 50 + preferences.FCS); ctx.fillText(`${delay_string}`, xfull - 75, y0 + 50 + preferences.FCS * 2); ctx.fillText(`DATA SERVER ${statusString()}`, xfull - 75, y0 + 50 + preferences.FCS * 3); ctx.fillText("MODE", xfull - 75, y0 + canvas.height / 3); // this hurts me physically if (selected_mode === "REF") { ctx.fillStyle = white; ctx.fillText(">REF< ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS); ctx.fillStyle = green; } else { ctx.fillText(" REF ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS); } if (selected_mode === "VEL") { ctx.fillStyle = white; ctx.fillText(" >VEL< ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS); ctx.fillStyle = green; } else { ctx.fillText(" VEL ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS); } if (selected_mode === "SW") { ctx.fillStyle = white; ctx.fillText(" >SW <", xfull - 75, y0 + canvas.height / 3 + preferences.FCS); ctx.fillStyle = green; } else { ctx.fillText(" SW ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS); } if (selected_mode === "ZDR") { ctx.fillStyle = white; ctx.fillText(">ZDR< ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*2); ctx.fillStyle = green; } else { ctx.fillText(" ZDR ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*2); } if (selected_mode === "PHI") { ctx.fillStyle = white; ctx.fillText(" >PHI< ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*2); ctx.fillStyle = green; } else { ctx.fillText(" PHI ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*2); } if (selected_mode === "RHO") { ctx.fillStyle = white; ctx.fillText(" >RHO<", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*2); ctx.fillStyle = green; } else { ctx.fillText(" RHO ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*2); } if (selected_mode === "CFP") { ctx.fillStyle = white; ctx.fillText(" >CFP< ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*3); ctx.fillStyle = green; } else { ctx.fillText(" CFP ", xfull - 75, y0 + canvas.height / 3 + preferences.FCS*3); } if (radar_inoperative) { ctx.fillStyle = red; ctx.fillText(" >RADR INOP<", xfull - 75, y0 + canvas.height / 3 + preferences.FCS * 3); ctx.fillStyle = green; } ctx.textAlign = "left"; for (let line = 0; line < display_buf.length; line++) { ctx.fillText(display_buf[line], x0 + 50, y0 + canvas.height / 2 + (preferences.FCS * line)); } } setInterval(reRender, 10); document.onkeyup = (e) => { if (e.key.toUpperCase() === "ESCAPE") { command_buf = ""; } else if (e.key.toUpperCase() === "ENTER") { let command = command_buf.replace("\n", " "); command_buf = ""; exec(command); } else if (e.key.toUpperCase() === "BACKSPACE") { command_buf = command_buf.slice(0, command_buf.length - 1); } else if (e.key.toUpperCase() === " " && shouldNewline()) { command_buf += "\n"; } else { if (e.key.length !== 1) { return; } if (buf_response_mode) { buf_response_mode = false; command_buf = ""; } command_buf += e.key.toUpperCase(); } recalcDisplayBuf(); reRender(); }