//@ts-check
import { hexColormapToRGBA, normalizeColorMap } from '../utils/colormap-utils.js';
import { isNumberMatrix, isStringArray } from '../utils/validation-utils.js';
import { WebGLRadarRenderer } from './webgl-radar-renderer.js';
import fragmentShaderSource from '../shaders/radar-fragment-shader.js';
import mercatorVertexShaderSource from '../shaders/mercator-vertex-shader.js';
import globeVertexShaderSource from '../shaders/globe-vertex-shader.js';
/**
* @class
*/
export class RadarRenderer {
/**
* @param {mapboxgl.Map} map a mapbox map object
* @param {string} layerId id of the layer to render the radar data
* @param {string} insertBeforeId layer to insert the new layer before
*/
constructor(map, layerId, insertBeforeId) {
this.map = map;
this.layerId = layerId;
this.insertBeforeId = insertBeforeId;
this.opacity = 1;
this.filter = { min: -Infinity, max: Infinity };
this.minValue = -10;
this.maxValue = 85;
/**
* @private
* @type {number[][]}*/
this.colormap = [];
/**
* @private
* @type {mapboxgl.CustomLayerInterface}*/
this.layer = null;
/**
* @private
* @type {Float32Array}*/
this.vertices = null;
/**
* @private
* @type {number[]}*/
this.mercatorVertices = [];
/**
* @private
* @type {number[]}*/
this.ecefVertices = [];
/**
* @private
* @type {RadarData}*/
this.radarData = null;
this.createLayer();
}
/**
* @private
* Create the custom layer for the radar data
*/
createLayer() {
this.layer = {
id: this.layerId,
type: 'custom',
onAdd: this.initWebGL.bind(this),
render: this.render.bind(this),
onRemove: this.destroy.bind(this)
};
this.map.on('load', () => {
this.map.addLayer(this.layer, this.insertBeforeId);
});
}
/**
* @private
* generate the vertex and fragment shaders for the radar data
* @param {mapboxgl.Map} map
* @param {WebGL2RenderingContext} gl
*/
initWebGL(map, gl) {
this.mercatorRenderer = new WebGLRadarRenderer(gl, mercatorVertexShaderSource, fragmentShaderSource);
this.globeRenderer = new WebGLRadarRenderer(gl, globeVertexShaderSource, fragmentShaderSource);
}
/**
* @private
* Render the radar data on the map
* @param {WebGL2RenderingContext} gl
* @param {Float32Array} projectionMatrix
* @param {mapboxgl.Projection} projection
* @param {Float32Array} globeToMercMatrix
* @param {number} transition
* @param {number[]} centerInMercator
* @param {number} pixelsPerMeterRatio
*/
render(gl, projectionMatrix, projection, globeToMercMatrix, transition, centerInMercator, pixelsPerMeterRatio) {
if (projection && projection.name == 'globe') {
this.globeRenderer.render({
u_projection: projectionMatrix,
u_globeToMercMatrix: globeToMercMatrix,
u_globeToMercatorTransition: transition,
u_centerInMercator: centerInMercator,
u_pixelsPerMeterRatio: pixelsPerMeterRatio
});
} else {
this.mercatorRenderer.render({ u_matrix: projectionMatrix });
}
}
/**
* Set the colormap for the radar data, the supported colormaps are hex RRGGBB or with alpha RRGGBBAA, or an array of RGB or RGBA values
* @param {string[] | number[][]} colormap
*/
setColormap(colormap) {
if (!this.mercatorRenderer) {
// if the webGLRenderer is not initialized, wait for it to be initialized
requestAnimationFrame(() => {
this.setColormap(colormap);
});
return;
}
if (isStringArray(colormap)) {
this.colormap = hexColormapToRGBA(colormap);
} else if (isNumberMatrix(colormap)) {
this.colormap = normalizeColorMap(colormap);
} else {
throw new Error('Invalid colormap format');
}
this.mercatorRenderer.setColormap(this.colormap);
this.globeRenderer.setColormap(this.colormap);
}
/**
* Set the minimum and maximum values for the radar data
* @param {number} min
* @param {number} max
*/
setMinMax(min, max) {
if (!this.mercatorRenderer) {
// if the webGLRenderer is not initialized, wait for it to be initialized
requestAnimationFrame(() => {
this.setMinMax(min, max);
});
return;
}
this.minValue = min;
this.maxValue = max;
this.globeRenderer.setMinMax(min, max);
this.mercatorRenderer.setMinMax(min, max);
}
/**
* Generate the vertices from the radar data
* @param {RadarData} radarData
* @returns {Float32Array}
*/
generateVertices(radarData) {
const { mapboxgl } = globalThis;
const radarAngle = radarData.azimuth_start - 90;
const vertices = new Float32Array(radarData.polar.length * radarData.polar[0].length * 6 * 3);
const center = mapboxgl.MercatorCoordinate.fromLngLat(radarData.location);
const firstGateOffset = radarData.meters_to_center_of_first_gate * center.meterInMercatorCoordinateUnits();
const gateSpacing = radarData.meters_between_gates * center.meterInMercatorCoordinateUnits();
// prevent memory allocation inside the loop
const tempPoints = [{ x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }, { x: 0, y: 0 }];
let angleIncrement = 360 / radarData.polar.length;
let layerCount = radarData.polar[0].length;
let gateStart = firstGateOffset;
let angle = 0;
for (let i = 0; i < radarData.polar.length; i += 1) {
angle = i * angleIncrement;
gateStart = firstGateOffset;
let layers = radarData.polar[i].slice(0, layerCount);
for (let l = 0; l < layers.length; l++) {
const value = layers[l];
gateStart += gateSpacing;
if (radarData.fill_value == value) continue;
if (value < this.filter.min || value > this.filter.max) continue;
const gateEnd = gateStart + gateSpacing;
const startAngle = (angle + radarAngle) * Math.PI / 180;
const finalAngle = (angle + angleIncrement + radarAngle) * Math.PI / 180;
let idx = (i * layerCount + l) * 6 * 3;
// first triangle
// first vertex
const x = Math.cos(startAngle) * gateStart;
const y = Math.sin(startAngle) * gateStart;
tempPoints[0].x = center.x + x;
tempPoints[0].y = center.y + y;
vertices[idx] = tempPoints[0].x;
vertices[idx + 1] = tempPoints[0].y;
vertices[idx + 2] = value;
// second vertex
const x2 = Math.cos(startAngle) * gateEnd;
const y2 = Math.sin(startAngle) * gateEnd;
tempPoints[1].x = center.x + x2;
tempPoints[1].y = center.y + y2;
vertices[idx + 3] = tempPoints[1].x;
vertices[idx + 4] = tempPoints[1].y;
vertices[idx + 5] = value;
// third vertex
const x3 = Math.cos(finalAngle) * gateStart;
const y3 = Math.sin(finalAngle) * gateStart;
tempPoints[2].x = center.x + x3;
tempPoints[2].y = center.y + y3;
vertices[idx + 6] = tempPoints[2].x;
vertices[idx + 7] = tempPoints[2].y;
vertices[idx + 8] = value;
// second triangle
// first vertex, same as third vertex of first triangle
vertices[idx + 9] = tempPoints[2].x;
vertices[idx + 10] = tempPoints[2].y;
vertices[idx + 11] = value;
// second vertex, same as second vertex of second triangle
vertices[idx + 12] = tempPoints[1].x;
vertices[idx + 13] = tempPoints[1].y;
vertices[idx + 14] = value;
// third vertex
const x4 = Math.cos(finalAngle) * gateEnd;
const y4 = Math.sin(finalAngle) * gateEnd;
tempPoints[3].x = center.x + x4;
tempPoints[3].y = center.y + y4;
vertices[idx + 15] = tempPoints[3].x;
vertices[idx + 16] = tempPoints[3].y;
vertices[idx + 17] = value;
}
}
return vertices;
}
/**
* Set a filter with the minimum and maximum values for the radar data
* @param {number} min
* @param {number} max
*/
setFilter(min, max) {
this.filter.min = min;
this.filter.max = max;
}
/**
* Set the opacity for the entire radar renderer
* @param {number} opacity
*/
setOpacity(opacity) {
if (!this.mercatorRenderer) {
// if the webGLRenderer is not initialized, wait for it to be initialized
requestAnimationFrame(() => {
this.setOpacity(opacity);
});
return;
}
this.opacity = opacity;
this.mercatorRenderer.setOpacity(opacity);
this.globeRenderer.setOpacity(opacity);
}
/**
* Draw the radar data on the map
* @param {RadarData} data
* @see {@link ./types/radar-data.d.ts}
*/
draw(data) {
this.radarData = data;
if (!this.mercatorRenderer) {
// if the webGLRenderer is not initialized, wait for it to be initialized
requestAnimationFrame(() => {
this.draw(data);
});
return;
}
this.vertices = this.generateVertices(data);
this.mercatorRenderer.setMinMax(this.minValue, this.maxValue);
this.mercatorRenderer.setAttributes({ a_pos: { data: this.vertices, size: 3 } });
this.globeRenderer.setAttributes({a_pos_merc: { data: this.vertices, size: 3}});
this.globeRenderer.setMinMax(this.minValue, this.maxValue);
}
/**
* Check if a point is inside a triangle
* @param {number} pointX position x of the point to check
* @param {number} pointY position y of the point to check
* @param {number} ax position x of the first vertex of the triangle
* @param {number} ay position y of the first vertex of the triangle
* @param {number} bx position x of the second vertex of the triangle
* @param {number} by position y of the second vertex of the triangle
* @param {number} cx position x of the third vertex of the triangle
* @param {number} cy position y of the third vertex of the triangle
* @returns {boolean}
*/
isPointInTriangle(pointX, pointY, ax, ay, bx, by, cx, cy) {
const v0 = [cx - ax, cy - ay];
const v1 = [bx - ax, by - ay];
const v2 = [pointX - ax, pointY - ay];
const dot00 = v0[0] * v0[0] + v0[1] * v0[1];
const dot01 = v0[0] * v1[0] + v0[1] * v1[1];
const dot02 = v0[0] * v2[0] + v0[1] * v2[1];
const dot11 = v1[0] * v1[0] + v1[1] * v1[1];
const dot12 = v1[0] * v2[0] + v1[1] * v2[1];
const invDenom = 1 / (dot00 * dot11 - dot01 * dot01);
const u = (dot11 * dot02 - dot01 * dot12) * invDenom;
const v = (dot00 * dot12 - dot01 * dot02) * invDenom;
return (u >= 0) && (v >= 0) && (u + v < 1);
}
/**
* get the RGBA color for a specific value
* @param {number} value
* @returns {number[]}
*/
getRGBAColor(value) {
const idx = Math.floor((value - this.minValue) / (this.maxValue - this.minValue) * (this.colormap.length - 1));
const color = this.colormap[idx];
return color;
}
/**
* get the hex color for a specific value
* @param {number} value
* @returns {string}
*/
getHexColor(value) {
const color = this.getRGBAColor(value);
return `#${color.map(c => c.toString(16).padStart(2, '0')).join('')}`;
}
/**
* Get the value at a specific latitude and longitude
* @param {[number, number]} lngLat
* @returns {{value:number, rgbaColor: number[], hexColor: string}}
*/
getValueAt(lngLat){
if(!this.vertices) return {
value: this.radarData.fill_value,
rgbaColor: [0, 0, 0, 0],
hexColor: "#00000000"
};
const point = globalThis.mapboxgl.MercatorCoordinate.fromLngLat(lngLat);
const vertices = this.vertices;
for(let i = 0; i < vertices.length; i += 18){
const ax = vertices[i];
const ay = vertices[i + 1];
const bx = vertices[i + 3];
const by = vertices[i + 4];
const cx = vertices[i + 6];
const cy = vertices[i + 7];
if(this.isPointInTriangle(point.x, point.y, ax, ay, bx, by, cx, cy)){
return {
value: vertices[i + 2],
rgbaColor: this.getRGBAColor(vertices[i + 2]),
hexColor: this.getHexColor(vertices[i + 2])
}
}
const dx = vertices[i + 9];
const dy = vertices[i + 10];
const ex = vertices[i + 12];
const ey = vertices[i + 13];
const fx = vertices[i + 15];
const fy = vertices[i + 16];
if(this.isPointInTriangle(point.x, point.y, dx, dy, ex, ey, fx, fy)){
return {
value: vertices[i + 11],
rgbaColor: this.getRGBAColor(vertices[i + 11]),
hexColor: this.getHexColor(vertices[i + 11])
}
}
}
return {
value: this.radarData.fill_value,
rgbaColor: [0, 0, 0, 0],
hexColor: "#00000000"
}
}
/**
* Clear the layer (just the visibility)
*/
clear() {
this.mercatorRenderer.clear();
this.globeRenderer.clear();
this.map.triggerRepaint();
}
/**
* Reset the radar renderer to its initial state, the colormap is not reset
*/
reset() {
this.filter.min = -Infinity;
this.filter.max = Infinity;
this.opacity = 1;
}
/**
* Destroy the radar renderer and cleanup
*/
destroy() {
this.map.removeLayer(this.layerId);
this.mercatorRenderer.destroy();
this.globeRenderer.destroy();
this.reset();
this.mercatorVertices = [];
}
}