From 2dcc33f2109a508887e1ae383d332b1a3c01b6fa Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Fri, 23 May 2025 23:13:50 -0500 Subject: [PATCH 1/6] change to offscreen canvas --- server/scripts/modules/radar.mjs | 39 +++++++++++++++-------- server/scripts/modules/weatherdisplay.mjs | 2 ++ views/partials/radar.ejs | 2 +- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/server/scripts/modules/radar.mjs b/server/scripts/modules/radar.mjs index dda0e53..665915d 100644 --- a/server/scripts/modules/radar.mjs +++ b/server/scripts/modules/radar.mjs @@ -119,18 +119,14 @@ class Radar extends WeatherDisplay { const radarSourceY = radarSourceXY.y / 2; // Load the most recent doppler radar images. - const radarInfo = await Promise.all(urls.map(async (url) => { + const radarInfo = await Promise.all(urls.map(async (url, index) => { // create destination context - const canvas = document.createElement('canvas'); - canvas.width = 640; - canvas.height = 367; + const canvas = new OffscreenCanvas(640, 367); const context = canvas.getContext('2d'); context.imageSmoothingEnabled = false; // create working context for manipulation - const workingCanvas = document.createElement('canvas'); - workingCanvas.width = width; - workingCanvas.height = height; + const workingCanvas = new OffscreenCanvas(width, height); const workingContext = workingCanvas.getContext('2d'); workingContext.imageSmoothingEnabled = false; @@ -161,32 +157,47 @@ class Radar extends WeatherDisplay { } else { time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone()); } - + console.time(`Radar-${index}`); // assign to an html image element + console.time(`Radar-${index}-loadimg`); const imgBlob = await loadImg(blob); - + console.timeEnd(`Radar-${index}-loadimg`); // draw the entire image workingContext.clearRect(0, 0, width, 1600); + console.time(`Radar-${index}-drawimage`); workingContext.drawImage(imgBlob, 0, 0, width, 1600); - + console.timeEnd(`Radar-${index}-drawimage`); // get the base map + console.time(`Radar-${index}-drawbasemap`); context.drawImage(this.baseMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367); - + console.timeEnd(`Radar-${index}-drawbasemap`); // crop the radar image const cropCanvas = document.createElement('canvas'); cropCanvas.width = 640; cropCanvas.height = 367; const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true }); cropContext.imageSmoothingEnabled = false; + console.time(`Radar-${index}-copy-radar`); cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367); - // clean the image + console.timeEnd(`Radar-${index}-copy-radar`); + // clean the im`age + console.time(`Radar-${index}-clean-image`); utils.removeDopplerRadarImageNoise(cropContext); + console.timeEnd(`Radar-${index}-clean-image`); // merge the radar and map + console.time(`Radar-${index}-merge`); utils.mergeDopplerRadarImage(context, cropContext); + console.timeEnd(`Radar-${index}-merge`); - const elem = this.fillTemplate('frame', { map: { type: 'img', src: canvas.toDataURL() } }); - + console.time(`Radar-${index}-dataurl`); + const onscreenCanvas = document.createElement('canvas'); + onscreenCanvas.width = canvas.width; + onscreenCanvas.height = canvas.height; + onscreenCanvas.getContext('bitmaprenderer').transferFromImageBitmap(canvas.transferToImageBitmap()); + const elem = this.fillTemplate('frame', { map: { type: 'canvas', canvas: onscreenCanvas } }); + console.timeEnd(`Radar-${index}-dataurl`); + console.timeEnd(`Radar-${index}`); return { canvas, time, diff --git a/server/scripts/modules/weatherdisplay.mjs b/server/scripts/modules/weatherdisplay.mjs index d9e3a07..34a5b75 100644 --- a/server/scripts/modules/weatherdisplay.mjs +++ b/server/scripts/modules/weatherdisplay.mjs @@ -421,6 +421,8 @@ class WeatherDisplay { } else if (value?.type === 'img') { // fill the image source elem.querySelector('img').src = value.src; + } else if (value?.type === 'canvas') { + elem.append(value.canvas); } }); diff --git a/views/partials/radar.ejs b/views/partials/radar.ejs index 5952f81..7b2c42d 100644 --- a/views/partials/radar.ejs +++ b/views/partials/radar.ejs @@ -35,7 +35,7 @@
- +
From 5567fe37a613ade222390ef5dddeb0eaf9c5a3c4 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sat, 24 May 2025 09:22:23 -0500 Subject: [PATCH 2/6] add instrumentation --- server/scripts/modules/radar.mjs | 63 ++++++++++++++------------ server/scripts/modules/utils/image.mjs | 8 ++++ 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/server/scripts/modules/radar.mjs b/server/scripts/modules/radar.mjs index 665915d..df072fc 100644 --- a/server/scripts/modules/radar.mjs +++ b/server/scripts/modules/radar.mjs @@ -1,7 +1,7 @@ // current weather conditions display import STATUS from './status.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs'; -import { loadImg } from './utils/image.mjs'; +import { loadImgElement, loadImg } from './utils/image.mjs'; import { text } from './utils/fetch.mjs'; import { rewriteUrl } from './utils/cors.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; @@ -54,7 +54,7 @@ class Radar extends WeatherDisplay { // get the base map const src = 'images/maps/radar.webp'; - this.baseMap = await loadImg(src); + this.baseMapImageElem = await loadImgElement(src); const baseUrl = `https://${RADAR_HOST}/archive/data/`; const baseUrlEnd = '/GIS/uscomp/?F=0&P=n0r*.png'; @@ -115,30 +115,35 @@ class Radar extends WeatherDisplay { const radarOffsetX = 120; const radarOffsetY = 70; const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY); - const radarSourceX = radarSourceXY.x / 2; - const radarSourceY = radarSourceXY.y / 2; + const radarSourceX = Math.round(radarSourceXY.x / 2); + const radarSourceY = Math.round(radarSourceXY.y / 2); // Load the most recent doppler radar images. const radarInfo = await Promise.all(urls.map(async (url, index) => { + console.time(`Radar-${index}`); // create destination context - const canvas = new OffscreenCanvas(640, 367); - const context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; + const baseCanvas = new OffscreenCanvas(640, 367); + const baseContext = baseCanvas.getContext('2d', { alpha: false }); + baseContext.imageSmoothingEnabled = false; // create working context for manipulation - const workingCanvas = new OffscreenCanvas(width, height); - const workingContext = workingCanvas.getContext('2d'); - workingContext.imageSmoothingEnabled = false; + const radarCanvas = new OffscreenCanvas(width, height); + const radarContext = radarCanvas.getContext('2d', { alpha: false }); + radarContext.imageSmoothingEnabled = false; // get the image const modifiedUrl = OVERRIDES.RADAR_HOST ? url.replace(RADAR_HOST, OVERRIDES.RADAR_HOST) : url; + console.time(`Radar-${index}-fetch`); const response = await fetch(rewriteUrl(modifiedUrl)); + console.timeEnd(`Radar-${index}-fetch`); // test response if (!response.ok) throw new Error(`Unable to fetch radar error ${response.status} ${response.statusText} from ${response.url}`); // get the blob - const blob = await response.blob(); + console.time(`Radar-${index}-blob`); + const radarImgBlob = await response.blob(); + console.timeEnd(`Radar-${index}-blob`); // store the time const timeMatch = url.match(/_(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)\./); @@ -157,49 +162,47 @@ class Radar extends WeatherDisplay { } else { time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone()); } - console.time(`Radar-${index}`); + // assign to an html image element - console.time(`Radar-${index}-loadimg`); - const imgBlob = await loadImg(blob); - console.timeEnd(`Radar-${index}-loadimg`); + console.time(`Radar-${index}-loadimg-element`); + const radarImgElement = await loadImg(radarImgBlob); + console.timeEnd(`Radar-${index}-loadimg-element`); // draw the entire image - workingContext.clearRect(0, 0, width, 1600); + radarContext.clearRect(0, 0, width, 1600); console.time(`Radar-${index}-drawimage`); - workingContext.drawImage(imgBlob, 0, 0, width, 1600); + radarContext.drawImage(radarImgElement, 0, 0, width, 1600); console.timeEnd(`Radar-${index}-drawimage`); // get the base map console.time(`Radar-${index}-drawbasemap`); - context.drawImage(this.baseMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367); + baseContext.drawImage(this.baseMapImageElem, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367); console.timeEnd(`Radar-${index}-drawbasemap`); // crop the radar image - const cropCanvas = document.createElement('canvas'); - cropCanvas.width = 640; - cropCanvas.height = 367; + const cropCanvas = new OffscreenCanvas(640, 367); const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true }); cropContext.imageSmoothingEnabled = false; console.time(`Radar-${index}-copy-radar`); - cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367); + cropContext.drawImage(radarCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), Math.round(radarOffsetY * 2.33), 0, 0, 640, 367); console.timeEnd(`Radar-${index}-copy-radar`); - // clean the im`age + // clean the image console.time(`Radar-${index}-clean-image`); utils.removeDopplerRadarImageNoise(cropContext); console.timeEnd(`Radar-${index}-clean-image`); // merge the radar and map console.time(`Radar-${index}-merge`); - utils.mergeDopplerRadarImage(context, cropContext); + utils.mergeDopplerRadarImage(baseContext, cropContext); console.timeEnd(`Radar-${index}-merge`); - console.time(`Radar-${index}-dataurl`); + console.time(`Radar-${index}-transfer-canvas`); const onscreenCanvas = document.createElement('canvas'); - onscreenCanvas.width = canvas.width; - onscreenCanvas.height = canvas.height; - onscreenCanvas.getContext('bitmaprenderer').transferFromImageBitmap(canvas.transferToImageBitmap()); + onscreenCanvas.width = baseCanvas.width; + onscreenCanvas.height = baseCanvas.height; + onscreenCanvas.getContext('bitmaprenderer').transferFromImageBitmap(baseCanvas.transferToImageBitmap()); const elem = this.fillTemplate('frame', { map: { type: 'canvas', canvas: onscreenCanvas } }); - console.timeEnd(`Radar-${index}-dataurl`); + console.timeEnd(`Radar-${index}-transfer-canvas`); console.timeEnd(`Radar-${index}`); return { - canvas, + canvas: baseCanvas, time, elem, }; diff --git a/server/scripts/modules/utils/image.mjs b/server/scripts/modules/utils/image.mjs index 1a7cabe..5d62363 100644 --- a/server/scripts/modules/utils/image.mjs +++ b/server/scripts/modules/utils/image.mjs @@ -28,7 +28,15 @@ const preloadImg = (src) => { return true; }; +const loadImgElement = (url) => new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => resolve(image); + image.onerror = reject; + image.src = url; +}); + export { loadImg, preloadImg, + loadImgElement, }; From 8d20f7672cc9f25c654cecdc34abc3842aefa2e9 Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sat, 24 May 2025 16:36:41 -0500 Subject: [PATCH 3/6] radar processed in web worker --- server/scripts/modules/radar-worker.mjs | 80 ++++++++++++++ server/scripts/modules/radar.mjs | 141 +++++++++--------------- 2 files changed, 134 insertions(+), 87 deletions(-) create mode 100644 server/scripts/modules/radar-worker.mjs diff --git a/server/scripts/modules/radar-worker.mjs b/server/scripts/modules/radar-worker.mjs new file mode 100644 index 0000000..d1894a0 --- /dev/null +++ b/server/scripts/modules/radar-worker.mjs @@ -0,0 +1,80 @@ +import * as utils from './radar-utils.mjs'; + +const fetchAsBlob = async (url) => { + const response = await fetch(url); + return response.blob(); +}; + +onmessage = async (e) => { + const baseMapImage = createImageBitmap(await fetchAsBlob('/images/maps/radar.webp')); + + const { + url, index, RADAR_HOST, OVERRIDES, radarSourceXY, sourceXY, offsetX, offsetY, + } = e.data; + + // calculate offsets and sizes + const width = 2550; + const height = 1600; + const radarOffsetX = 120; + const radarOffsetY = 70; + const radarSourceX = Math.round(radarSourceXY.x / 2); + const radarSourceY = Math.round(radarSourceXY.y / 2); + + // create destination context + const baseCanvas = new OffscreenCanvas(640, 367); + const baseContext = baseCanvas.getContext('2d', { alpha: false }); + baseContext.imageSmoothingEnabled = false; + + // create working context for manipulation + const radarCanvas = new OffscreenCanvas(width, height); + const radarContext = radarCanvas.getContext('2d', { alpha: false }); + radarContext.imageSmoothingEnabled = false; + + // get the image + const modifiedUrl = OVERRIDES.RADAR_HOST ? url.replace(RADAR_HOST, OVERRIDES.RADAR_HOST) : url; + console.time(`Radar-${index}-fetch`); + const response = await fetch(modifiedUrl); + console.timeEnd(`Radar-${index}-fetch`); + + // test response + if (!response.ok) throw new Error(`Unable to fetch radar error ${response.status} ${response.statusText} from ${response.url}`); + + // get the blob + console.time(`Radar-${index}-blob`); + const radarImgBlob = await response.blob(); + console.timeEnd(`Radar-${index}-blob`); + + // assign to an html image element + console.time(`Radar-${index}-loadimg-element`); + const radarImgElement = await createImageBitmap(radarImgBlob); + console.timeEnd(`Radar-${index}-loadimg-element`); + // draw the entire image + radarContext.clearRect(0, 0, width, 1600); + console.time(`Radar-${index}-drawimage`); + radarContext.drawImage(radarImgElement, 0, 0, width, 1600); + console.timeEnd(`Radar-${index}-drawimage`); + // get the base map + console.time(`Radar-${index}-drawbasemap`); + baseContext.drawImage(await baseMapImage, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367); + console.timeEnd(`Radar-${index}-drawbasemap`); + // crop the radar image + const cropCanvas = new OffscreenCanvas(640, 367); + const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true }); + cropContext.imageSmoothingEnabled = false; + console.time(`Radar-${index}-copy-radar`); + cropContext.drawImage(radarCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), Math.round(radarOffsetY * 2.33), 0, 0, 640, 367); + console.timeEnd(`Radar-${index}-copy-radar`); + // clean the image + console.time(`Radar-${index}-clean-image`); + utils.removeDopplerRadarImageNoise(cropContext); + console.timeEnd(`Radar-${index}-clean-image`); + + // merge the radar and map + console.time(`Radar-${index}-merge`); + utils.mergeDopplerRadarImage(baseContext, cropContext); + console.timeEnd(`Radar-${index}-merge`); + + const processedRadar = baseCanvas.transferToImageBitmap(); + + postMessage(processedRadar, [processedRadar]); +}; diff --git a/server/scripts/modules/radar.mjs b/server/scripts/modules/radar.mjs index df072fc..2953aa1 100644 --- a/server/scripts/modules/radar.mjs +++ b/server/scripts/modules/radar.mjs @@ -1,9 +1,8 @@ // current weather conditions display import STATUS from './status.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs'; -import { loadImgElement, loadImg } from './utils/image.mjs'; +import { loadImgElement } from './utils/image.mjs'; import { text } from './utils/fetch.mjs'; -import { rewriteUrl } from './utils/cors.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay, timeZone } from './navigation.mjs'; import * as utils from './radar-utils.mjs'; @@ -41,6 +40,9 @@ class Radar extends WeatherDisplay { { time: 1, si: 4 }, { time: 12, si: 5 }, ]; + + // get some web workers started + this.workers = (new Array(this.dopplerRadarImageMax)).fill(null).map(() => radarWorker()); } async getData(weatherParameters, refresh) { @@ -52,10 +54,6 @@ class Radar extends WeatherDisplay { return; } - // get the base map - const src = 'images/maps/radar.webp'; - this.baseMapImageElem = await loadImgElement(src); - const baseUrl = `https://${RADAR_HOST}/archive/data/`; const baseUrlEnd = '/GIS/uscomp/?F=0&P=n0r*.png'; const baseUrls = []; @@ -105,104 +103,50 @@ class Radar extends WeatherDisplay { // calculate offsets and sizes let offsetX = 120; let offsetY = 69; - const width = 2550; - const height = 1600; offsetX *= 2; offsetY *= 2; const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters, offsetX, offsetY); - - // calculate radar offsets - const radarOffsetX = 120; - const radarOffsetY = 70; const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY); - const radarSourceX = Math.round(radarSourceXY.x / 2); - const radarSourceY = Math.round(radarSourceXY.y / 2); // Load the most recent doppler radar images. const radarInfo = await Promise.all(urls.map(async (url, index) => { console.time(`Radar-${index}`); - // create destination context - const baseCanvas = new OffscreenCanvas(640, 367); - const baseContext = baseCanvas.getContext('2d', { alpha: false }); - baseContext.imageSmoothingEnabled = false; - - // create working context for manipulation - const radarCanvas = new OffscreenCanvas(width, height); - const radarContext = radarCanvas.getContext('2d', { alpha: false }); - radarContext.imageSmoothingEnabled = false; - - // get the image - const modifiedUrl = OVERRIDES.RADAR_HOST ? url.replace(RADAR_HOST, OVERRIDES.RADAR_HOST) : url; - console.time(`Radar-${index}-fetch`); - const response = await fetch(rewriteUrl(modifiedUrl)); - console.timeEnd(`Radar-${index}-fetch`); - - // test response - if (!response.ok) throw new Error(`Unable to fetch radar error ${response.status} ${response.statusText} from ${response.url}`); - - // get the blob - console.time(`Radar-${index}-blob`); - const radarImgBlob = await response.blob(); - console.timeEnd(`Radar-${index}-blob`); + const processedRadar = await this.workers[index].processRadar({ + url, + index, + RADAR_HOST, + OVERRIDES, + sourceXY, + radarSourceXY, + offsetX, + offsetY, + }); // store the time const timeMatch = url.match(/_(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)\./); - let time; - if (timeMatch) { - const [, year, month, day, hour, minute] = timeMatch; - time = DateTime.fromObject({ - year, - month, - day, - hour, - minute, - }, { - zone: 'UTC', - }).setZone(timeZone()); - } else { - time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone()); - } - // assign to an html image element - console.time(`Radar-${index}-loadimg-element`); - const radarImgElement = await loadImg(radarImgBlob); - console.timeEnd(`Radar-${index}-loadimg-element`); - // draw the entire image - radarContext.clearRect(0, 0, width, 1600); - console.time(`Radar-${index}-drawimage`); - radarContext.drawImage(radarImgElement, 0, 0, width, 1600); - console.timeEnd(`Radar-${index}-drawimage`); - // get the base map - console.time(`Radar-${index}-drawbasemap`); - baseContext.drawImage(this.baseMapImageElem, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367); - console.timeEnd(`Radar-${index}-drawbasemap`); - // crop the radar image - const cropCanvas = new OffscreenCanvas(640, 367); - const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true }); - cropContext.imageSmoothingEnabled = false; - console.time(`Radar-${index}-copy-radar`); - cropContext.drawImage(radarCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), Math.round(radarOffsetY * 2.33), 0, 0, 640, 367); - console.timeEnd(`Radar-${index}-copy-radar`); - // clean the image - console.time(`Radar-${index}-clean-image`); - utils.removeDopplerRadarImageNoise(cropContext); - console.timeEnd(`Radar-${index}-clean-image`); - - // merge the radar and map - console.time(`Radar-${index}-merge`); - utils.mergeDopplerRadarImage(baseContext, cropContext); - console.timeEnd(`Radar-${index}-merge`); + const [, year, month, day, hour, minute] = timeMatch; + const time = DateTime.fromObject({ + year, + month, + day, + hour, + minute, + }, { + zone: 'UTC', + }).setZone(timeZone()); console.time(`Radar-${index}-transfer-canvas`); const onscreenCanvas = document.createElement('canvas'); - onscreenCanvas.width = baseCanvas.width; - onscreenCanvas.height = baseCanvas.height; - onscreenCanvas.getContext('bitmaprenderer').transferFromImageBitmap(baseCanvas.transferToImageBitmap()); + onscreenCanvas.width = 640; + onscreenCanvas.height = 367; + const onscreenContext = onscreenCanvas.getContext('bitmaprenderer'); + onscreenContext.transferFromImageBitmap(processedRadar); + const elem = this.fillTemplate('frame', { map: { type: 'canvas', canvas: onscreenCanvas } }); console.timeEnd(`Radar-${index}-transfer-canvas`); console.timeEnd(`Radar-${index}`); return { - canvas: baseCanvas, time, elem, }; @@ -215,8 +159,6 @@ class Radar extends WeatherDisplay { // set max length this.timing.totalScreens = radarInfo.length; - // store the images - this.data = radarInfo.map((radar) => radar.canvas); this.times = radarInfo.map((radar) => radar.time); this.setStatus(STATUS.loaded); @@ -239,5 +181,30 @@ class Radar extends WeatherDisplay { } } +// create a radar worker with helper functions +const radarWorker = () => { + // create the worker + const worker = new Worker('scripts/modules/radar-worker.mjs', { type: 'module' }); + + const processRadar = (url) => new Promise((resolve, reject) => { + // prepare for done message + worker.onmessage = (e) => { + if (e?.data instanceof Error) { + reject(e.data); + } else if (e?.data instanceof ImageBitmap) { + resolve(e.data); + } + }; + + // start up the worker + worker.postMessage(url); + }); + + // return the object + return { + processRadar, + }; +}; + // register display registerDisplay(new Radar(11, 'radar')); From 002e037bbd5dc507f1463b4711e5a737ccea83ba Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sun, 25 May 2025 15:17:56 -0500 Subject: [PATCH 4/6] optimized radar image merging --- server/scripts/modules/radar-worker.mjs | 122 +++++++++++++++--------- server/scripts/modules/radar.mjs | 12 +-- 2 files changed, 78 insertions(+), 56 deletions(-) diff --git a/server/scripts/modules/radar-worker.mjs b/server/scripts/modules/radar-worker.mjs index d1894a0..ee03098 100644 --- a/server/scripts/modules/radar-worker.mjs +++ b/server/scripts/modules/radar-worker.mjs @@ -1,78 +1,106 @@ import * as utils from './radar-utils.mjs'; +const radarFullSize = { width: 2550, height: 1600 }; +const radarFinalSize = { width: 640, height: 367 }; + const fetchAsBlob = async (url) => { const response = await fetch(url); return response.blob(); }; -onmessage = async (e) => { - const baseMapImage = createImageBitmap(await fetchAsBlob('/images/maps/radar.webp')); +const baseMapImages = new Promise((resolve) => { + fetchAsBlob('/images/maps/radar.webp').then((blob) => { + createImageBitmap(blob).then((imageBitmap) => { + // extract the black pixels to overlay on to the final image (boundaries) + console.time('radar-overlay'); + const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height); + const context = canvas.getContext('2d'); + context.drawImage(imageBitmap, 0, 0); + const imageData = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height); + // go through the image data and preserve the black pixels, making the rest transparent + for (let i = 0; i < imageData.data.length; i += 4) { + if (imageData.data[i + 0] >= 116 || imageData.data[i + 1] >= 116 || imageData.data[i + 2] >= 116) { + // make it transparent + imageData.data[i + 3] = 0; + } + } + // write the image data back + context.putImageData(imageData, 0, 0); + + console.timeEnd('radar-overlay'); + resolve({ + fullMap: imageBitmap, + overlay: canvas, + }); + }); + }); +}); + +onmessage = async (e) => { const { - url, index, RADAR_HOST, OVERRIDES, radarSourceXY, sourceXY, offsetX, offsetY, + url, RADAR_HOST, OVERRIDES, radarSourceXY, sourceXY, offsetX, offsetY, } = e.data; + // get the image + const modifiedRadarUrl = OVERRIDES.RADAR_HOST ? url.replace(RADAR_HOST, OVERRIDES.RADAR_HOST) : url; + const radarResponsePromise = fetch(modifiedRadarUrl); + // calculate offsets and sizes - const width = 2550; - const height = 1600; - const radarOffsetX = 120; - const radarOffsetY = 70; - const radarSourceX = Math.round(radarSourceXY.x / 2); - const radarSourceY = Math.round(radarSourceXY.y / 2); + + const radarSource = { + width: 240, + height: 163, + x: Math.round(radarSourceXY.x / 2), + y: Math.round(radarSourceXY.y / 2), + }; // create destination context - const baseCanvas = new OffscreenCanvas(640, 367); - const baseContext = baseCanvas.getContext('2d', { alpha: false }); + const baseCanvas = new OffscreenCanvas(radarFinalSize.width, radarFinalSize.height); + const baseContext = baseCanvas.getContext('2d'); baseContext.imageSmoothingEnabled = false; // create working context for manipulation - const radarCanvas = new OffscreenCanvas(width, height); - const radarContext = radarCanvas.getContext('2d', { alpha: false }); + const radarCanvas = new OffscreenCanvas(radarFullSize.width, radarFullSize.height); + const radarContext = radarCanvas.getContext('2d'); radarContext.imageSmoothingEnabled = false; - // get the image - const modifiedUrl = OVERRIDES.RADAR_HOST ? url.replace(RADAR_HOST, OVERRIDES.RADAR_HOST) : url; - console.time(`Radar-${index}-fetch`); - const response = await fetch(modifiedUrl); - console.timeEnd(`Radar-${index}-fetch`); + // get the base map + const baseMaps = await baseMapImages; + baseContext.drawImage(baseMaps.fullMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, radarFinalSize.width, radarFinalSize.height); // test response - if (!response.ok) throw new Error(`Unable to fetch radar error ${response.status} ${response.statusText} from ${response.url}`); + const radarResponse = await radarResponsePromise; + if (!radarResponse.ok) throw new Error(`Unable to fetch radar error ${radarResponse.status} ${radarResponse.statusText} from ${radarResponse.url}`); // get the blob - console.time(`Radar-${index}-blob`); - const radarImgBlob = await response.blob(); - console.timeEnd(`Radar-${index}-blob`); + const radarImgBlob = await radarResponse.blob(); // assign to an html image element - console.time(`Radar-${index}-loadimg-element`); const radarImgElement = await createImageBitmap(radarImgBlob); - console.timeEnd(`Radar-${index}-loadimg-element`); // draw the entire image - radarContext.clearRect(0, 0, width, 1600); - console.time(`Radar-${index}-drawimage`); - radarContext.drawImage(radarImgElement, 0, 0, width, 1600); - console.timeEnd(`Radar-${index}-drawimage`); - // get the base map - console.time(`Radar-${index}-drawbasemap`); - baseContext.drawImage(await baseMapImage, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367); - console.timeEnd(`Radar-${index}-drawbasemap`); - // crop the radar image - const cropCanvas = new OffscreenCanvas(640, 367); - const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true }); - cropContext.imageSmoothingEnabled = false; - console.time(`Radar-${index}-copy-radar`); - cropContext.drawImage(radarCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), Math.round(radarOffsetY * 2.33), 0, 0, 640, 367); - console.timeEnd(`Radar-${index}-copy-radar`); - // clean the image - console.time(`Radar-${index}-clean-image`); - utils.removeDopplerRadarImageNoise(cropContext); - console.timeEnd(`Radar-${index}-clean-image`); + radarContext.clearRect(0, 0, radarFullSize.width, radarFullSize.height); + radarContext.drawImage(radarImgElement, 0, 0, radarFullSize.width, radarFullSize.height); - // merge the radar and map - console.time(`Radar-${index}-merge`); - utils.mergeDopplerRadarImage(baseContext, cropContext); - console.timeEnd(`Radar-${index}-merge`); + // crop the radar image without scaling + const croppedRadarCanvas = new OffscreenCanvas(radarSource.width, radarSource.height); + const croppedRadarContext = croppedRadarCanvas.getContext('2d'); + croppedRadarContext.imageSmoothingEnabled = false; + croppedRadarContext.drawImage(radarCanvas, radarSource.x, radarSource.y, croppedRadarCanvas.width, croppedRadarCanvas.height, 0, 0, croppedRadarCanvas.width, croppedRadarCanvas.height); + + // clean the image + utils.removeDopplerRadarImageNoise(croppedRadarContext); + + // stretch the radar image + const stretchCanvas = new OffscreenCanvas(radarFinalSize.width, radarFinalSize.height); + const stretchContext = stretchCanvas.getContext('2d', { willReadFrequently: true }); + stretchContext.imageSmoothingEnabled = false; + stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, radarFinalSize.width, radarFinalSize.height); + + // put the radar on the base map + baseContext.drawImage(stretchCanvas, 0, 0); + // put the road/boundaries overlay on the map + baseContext.drawImage(baseMaps.overlay, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, radarFinalSize.width, radarFinalSize.height); const processedRadar = baseCanvas.transferToImageBitmap(); diff --git a/server/scripts/modules/radar.mjs b/server/scripts/modules/radar.mjs index 2953aa1..cca8cdd 100644 --- a/server/scripts/modules/radar.mjs +++ b/server/scripts/modules/radar.mjs @@ -1,7 +1,6 @@ // current weather conditions display import STATUS from './status.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs'; -import { loadImgElement } from './utils/image.mjs'; import { text } from './utils/fetch.mjs'; import WeatherDisplay from './weatherdisplay.mjs'; import { registerDisplay, timeZone } from './navigation.mjs'; @@ -110,10 +109,8 @@ class Radar extends WeatherDisplay { // Load the most recent doppler radar images. const radarInfo = await Promise.all(urls.map(async (url, index) => { - console.time(`Radar-${index}`); const processedRadar = await this.workers[index].processRadar({ url, - index, RADAR_HOST, OVERRIDES, sourceXY, @@ -136,16 +133,13 @@ class Radar extends WeatherDisplay { zone: 'UTC', }).setZone(timeZone()); - console.time(`Radar-${index}-transfer-canvas`); const onscreenCanvas = document.createElement('canvas'); - onscreenCanvas.width = 640; - onscreenCanvas.height = 367; + onscreenCanvas.width = processedRadar.width; + onscreenCanvas.height = processedRadar.height; const onscreenContext = onscreenCanvas.getContext('bitmaprenderer'); onscreenContext.transferFromImageBitmap(processedRadar); const elem = this.fillTemplate('frame', { map: { type: 'canvas', canvas: onscreenCanvas } }); - console.timeEnd(`Radar-${index}-transfer-canvas`); - console.timeEnd(`Radar-${index}`); return { time, elem, @@ -184,7 +178,7 @@ class Radar extends WeatherDisplay { // create a radar worker with helper functions const radarWorker = () => { // create the worker - const worker = new Worker('scripts/modules/radar-worker.mjs', { type: 'module' }); + const worker = new Worker(new URL('./radar-worker.mjs', import.meta.url), { type: 'module' }); const processRadar = (url) => new Promise((resolve, reject) => { // prepare for done message From 25626a98c9a8e77f0424d233d85363586d0bb30e Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sun, 25 May 2025 15:18:32 -0500 Subject: [PATCH 5/6] 5.21.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3781b6c..de46792 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ws4kp", - "version": "5.20.5", + "version": "5.21.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ws4kp", - "version": "5.20.5", + "version": "5.21.0", "license": "MIT", "dependencies": { "dotenv": "^16.5.0", diff --git a/package.json b/package.json index b0febb5..02506bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ws4kp", - "version": "5.20.5", + "version": "5.21.0", "description": "Welcome to the WeatherStar 4000+ project page!", "main": "index.mjs", "type": "module", From 25ac2059a6d0e789967574014a2dbd9db5ba6d3d Mon Sep 17 00:00:00 2001 From: Matt Walsh Date: Sun, 25 May 2025 19:11:52 -0500 Subject: [PATCH 6/6] remove debug timing --- server/scripts/modules/radar-worker.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/scripts/modules/radar-worker.mjs b/server/scripts/modules/radar-worker.mjs index ee03098..75cc6e9 100644 --- a/server/scripts/modules/radar-worker.mjs +++ b/server/scripts/modules/radar-worker.mjs @@ -12,7 +12,6 @@ const baseMapImages = new Promise((resolve) => { fetchAsBlob('/images/maps/radar.webp').then((blob) => { createImageBitmap(blob).then((imageBitmap) => { // extract the black pixels to overlay on to the final image (boundaries) - console.time('radar-overlay'); const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height); const context = canvas.getContext('2d'); context.drawImage(imageBitmap, 0, 0); @@ -28,7 +27,6 @@ const baseMapImages = new Promise((resolve) => { // write the image data back context.putImageData(imageData, 0, 0); - console.timeEnd('radar-overlay'); resolve({ fullMap: imageBitmap, overlay: canvas,