diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index bbbbbd9..8f3fb78 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -7,11 +7,11 @@
-
+
+
-
@@ -19,7 +19,7 @@
-
+
+
+
+ 1748807673267
+
+
+
+ 1748807673267
+
+
@@ -192,6 +201,7 @@
-
+
+
\ No newline at end of file
diff --git a/client/src/lib/Map.svelte b/client/src/lib/Map.svelte
index d1122f5..0eaa620 100644
--- a/client/src/lib/Map.svelte
+++ b/client/src/lib/Map.svelte
@@ -10,18 +10,7 @@
import CloudAlertIcon from '@lucide/svelte/icons/cloud-alert';
import { borderLUT, fillLUT } from '$lib/alertLayer';
import AlertPopup from '$lib/AlertPopup.svelte';
- import { CifContainer } from '$lib/generated_interop/cifContainer';
- import fragmentSource from '$lib/map/fragment.glsl?raw';
- import vertexSource from '$lib/map/vertex.glsl?raw';
- import { degreesToRadians } from '@turf/turf';
- import type { DigitalRadarData, Radial } from './generated_interop/digitalRadarData';
- import {forwardGeodesic, radToDeg} from "$lib/vincenty";
- import {degToRad} from "$lib/vincenty.js";
- import init_palette_parser_wasm, {parse_palette} from "$lib/map/pal/wxbox_pal";
- import {WXBOX_STANDARD_REF} from "$lib/map/default_palettes";
- const BELOW_THRESHOLD = -9999.0;
- const RANGE_FOLDED = -9998.0;
interface Props {
categories: LayerList;
@@ -161,213 +150,6 @@
map.getCanvas().style.cursor = '';
popup.remove();
});
-
- let r = await fetch('http://localhost:3000/v2/nexrad/l2/KLWX/1/REF');
- const buf = await r.arrayBuffer();
-
- const container = CifContainer.fromBinary(new Uint8Array(buf));
- console.log('wxrad http://localhost:3000/v2/nexrad/l2/KLWX/1/REF: ', container);
- if (container.messageType.oneofKind == 'digitalRadarData') {
- const drd: DigitalRadarData = container.messageType.digitalRadarData;
-
- await init_palette_parser_wasm();
-
- const dataLayer = {
- id: 'dataGl',
- type: 'custom',
- maxZoom: 13,
- onAdd(map, gl) {
- const vertexShader = gl.createShader(gl.VERTEX_SHADER);
- gl.shaderSource(vertexShader, vertexSource);
- gl.compileShader(vertexShader);
-
- const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
- gl.shaderSource(fragmentShader, fragmentSource);
- gl.compileShader(fragmentShader);
-
- this.program = gl.createProgram();
- gl.attachShader(this.program, vertexShader);
- gl.attachShader(this.program, fragmentShader);
- gl.linkProgram(this.program);
- gl.useProgram(this.program);
-
- this.aPos = gl.getAttribLocation(this.program, 'a_pos');
-
- const lat = 38.976111;
- const long = -77.4875;
-
- gl.uniform1f(gl.getUniformLocation(this.program, 'radarLat'), lat);
- gl.uniform1f(gl.getUniformLocation(this.program, 'radarLng'), long);
-
- const vertexData: number[] = [];
- const triangleAzimuthLookup: number[] = [];
-
- const radarCoordinate = maplibregl.MercatorCoordinate.fromLngLat({
- lng: long,
- lat: lat
- });
-
- for (let i =0; i < drd.radials.length; i++) {
- const radial = drd.radials[i];
-
- if (radial.product && radial.product.data && radial.product.data.data) {
- const angle_a = radial.azimuthAngleDegrees;
- const angle_b = (i == drd.radials.length - 1) ? drd.radials[0].azimuthAngleDegrees : drd.radials[i+1].azimuthAngleDegrees;
-
- const start_range = radial.product.data.startRange; // m
- const sample_interval = radial.product.data.sampleInterval; // m
- const number_of_samples = radial.product.data.data.length;
- const range = sample_interval * number_of_samples + start_range; // m
- // add an extra sample for good measure
- const padded_range = range + sample_interval; // m
-
- const in_weird_mercator_units = padded_range / radarCoordinate.meterInMercatorCoordinateUnits();
- const pointAX = radarCoordinate.x + in_weird_mercator_units * Math.sin(degToRad(-angle_a + 180));
- const pointAY = radarCoordinate.y + in_weird_mercator_units * Math.cos(degToRad(-angle_a + 180));
- const pointA = new maplibregl.MercatorCoordinate(pointAX, pointAY, 0);
-
- const pointBX = radarCoordinate.x + in_weird_mercator_units * Math.sin(degToRad(-angle_b + 180));
- const pointBY = radarCoordinate.y + in_weird_mercator_units * Math.cos(degToRad(-angle_b + 180));
- const pointB = new maplibregl.MercatorCoordinate(pointBX, pointBY, 0);
-
- vertexData.push(radarCoordinate.x);
- vertexData.push(radarCoordinate.y);
- vertexData.push(pointA.x);
- vertexData.push(pointA.y);
- vertexData.push(pointB.x);
- vertexData.push(pointB.y);
- triangleAzimuthLookup.push(angle_a);
- }
- }
-
- const radar_range_maximum = 560; // ish km
- const degrees_of_lat = radar_range_maximum / 110.574;
- const degrees_of_long =
- radar_range_maximum / (111.32 * Math.cos(degreesToRadians(lat)));
-
- this.buffer = gl.createBuffer();
- gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
- gl.bufferData(
- gl.ARRAY_BUFFER,
- new Float32Array(vertexData),
- gl.STATIC_DRAW
- );
- this.vertexCount = vertexData.length/2;
- gl.uniform1fv(gl.getUniformLocation(this.program, 'triangleAzimuthLookup'), new Float32Array(triangleAzimuthLookup));
-
- function scaleMomentData(radial: Radial, u: number): number {
- if (!radial.product || !radial.product.data) return BELOW_THRESHOLD;
- if (radial.product?.data?.scale == 0) {
- return u;
- } else {
- if (u == 0) {
- return BELOW_THRESHOLD;
- } else if (u == 1) {
- return RANGE_FOLDED;
- } else {
- return (u - radial.product.data?.offset) / radial.product?.data?.scale;
- }
- }
- }
- const raw_data = [];
- let validRadials = 0;
-
- for (const radial of drd.radials) {
- if (radial.product && radial.product.data && radial.product.data.data) {
- for (const datapoint of radial.product.data.data) {
- raw_data.push(scaleMomentData(radial, datapoint));
- }
- validRadials++;
- }
- }
-
- const data = new Float32Array(raw_data);
- gl.activeTexture(gl.TEXTURE0 + 10);
- this.texture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, 1832, validRadials, 0, gl.RED, gl.FLOAT, data);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- gl.uniform1i(gl.getUniformLocation(this.program, 'data'), 10);
-
- // parse, transform, and send over the palette
- const palette = parse_palette(WXBOX_STANDARD_REF);
- gl.uniform1i(gl.getUniformLocation(this.program, 'paletteLength'), palette.length);
- const rboundsData: number[] = [];
- const rcolorData: number[] = [];
- const rcolor2Data: number[] = [];
- for (const paletteEntry of palette) {
- const [bounds, color] = paletteEntry;
- rboundsData.push(...bounds);
- rcolorData.push(...[color[0].red, color[0].green, color[0].blue, color[0].alpha]);
- rcolor2Data.push(...[color[1].red, color[1].green, color[1].blue, color[1].alpha]);
- }
- // create the bounds texture first
- const boundsData = new Float32Array(rboundsData);
- gl.activeTexture(gl.TEXTURE0 + 11);
- this.boundsTexture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, this.boundsTexture);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, 1, palette.length, 0, gl.RG, gl.FLOAT, boundsData);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- gl.uniform1i(gl.getUniformLocation(this.program, 'bounds'), 11);
- // then the colorData texture
- const colorData = new Uint8Array(rcolorData);
- gl.activeTexture(gl.TEXTURE0 + 12);
- this.colorDataTexture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, this.colorDataTexture);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, palette.length, 0, gl.RGBA, gl.UNSIGNED_BYTE, colorData);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- gl.uniform1i(gl.getUniformLocation(this.program, 'colorData'), 12);
- // then the colorData2 texture
- const color2Data = new Uint8Array(rcolor2Data);
- gl.activeTexture(gl.TEXTURE0 + 13);
- this.color2DataTexture = gl.createTexture();
- gl.bindTexture(gl.TEXTURE_2D, this.color2DataTexture);
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, palette.length, 0, gl.RGBA, gl.UNSIGNED_BYTE, color2Data);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
- gl.uniform1i(gl.getUniformLocation(this.program, 'color2Data'), 13);
- },
- render(gl, args) {
- gl.enable(gl.SAMPLE_COVERAGE);
- gl.sampleCoverage(1.0, false);
-
- gl.activeTexture(gl.TEXTURE0 + 10);
- gl.bindTexture(gl.TEXTURE_2D, this.texture);
- gl.activeTexture(gl.TEXTURE0 + 11);
- gl.bindTexture(gl.TEXTURE_2D, this.boundsTexture);
- gl.activeTexture(gl.TEXTURE0 + 12);
- gl.bindTexture(gl.TEXTURE_2D, this.colorDataTexture);
- gl.activeTexture(gl.TEXTURE0 + 13);
- gl.bindTexture(gl.TEXTURE_2D, this.color2DataTexture);
- gl.useProgram(this.program);
-
- gl.uniformMatrix4fv(
- gl.getUniformLocation(this.program, 'u_matrix'),
- false,
- args.defaultProjectionData.mainMatrix
- );
-
- gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
- gl.enableVertexAttribArray(this.aPos);
- gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
- gl.enable(gl.BLEND);
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
- gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
- }
- };
- map.addLayer(dataLayer, 'alerts');
- }
});
map.on('error', (e) => {
console.error(e);
diff --git a/client/src/lib/ToolbarProductSelector.svelte b/client/src/lib/ToolbarProductSelector.svelte
index 2a04632..0584e4d 100644
--- a/client/src/lib/ToolbarProductSelector.svelte
+++ b/client/src/lib/ToolbarProductSelector.svelte
@@ -3,8 +3,9 @@
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { buttonVariants } from '$lib/components/ui/button';
- import type { LayerList, PrimaryLayer } from './layerList';
+ import type {Category, LayerList, PrimaryLayer} from './layerList';
import { stationGeojson, type StationStatus } from './stationData';
+ import {plotRadial} from "$lib/map/map";
interface Props {
selectedPrimaryLayer: string | null;
@@ -29,6 +30,118 @@
let currentlySelectingFor: string | null = $state(null);
let currentlySelectingCategory: PrimaryLayer[] | null = $state(null);
let open: boolean = $state(false);
+
+ let stationLat: number | null = $state(null);
+ let stationLong: number | null = $state(null);
+
+ function showSinglesite(data: PrimaryLayer) {
+ selectedPrimaryLayer = data.id;
+ idToNameCache[data.id] = data.layer;
+
+ if (map.getLayer("data")) {
+ map.removeLayer('data');
+ }
+ plotRadial(map, selectedSite, "REF", 1, stationLat, stationLong);
+ }
+
+ function clearDatalayer() {
+ selectedPrimaryLayer = null;
+ if (map.getLayer('data')) {
+ map.removeLayer('data')
+ }
+ }
+
+ function selectSiteForSinglesite(item: Category) {
+ if (currentlySelectingFor) {
+ if (map.getLayer(`select-${currentlySelectingFor}`)) {
+ map.removeLayer(`select-${currentlySelectingFor}`);
+ }
+ }
+ selectedPrimaryLayer = item.id;
+ idToNameCache[item.id] = item.category;
+ pickingSiteForCategory = true;
+ currentlySelectingCategory = item.children;
+ selectedSite = null;
+ if (!map.getSource(item.id)) {
+ map.addSource(item.id, {
+ type: 'geojson',
+ data: stationGeojson(stations)
+ });
+ }
+ map.addLayer({
+ id: `select-${item.id}`,
+ type: 'symbol',
+ source: item.id,
+ layout: {
+ 'icon-image': ['get', 'icon'],
+ 'text-field': ['get', 'icao'],
+ 'text-size': 13,
+ 'icon-size': 0.23
+ },
+ paint: {
+ 'text-color': '#000000'
+ }
+ });
+ currentlySelectingFor = item.id;
+ map.on('click', `select-${item.id}`, (e) => {
+ if (e && e.features) {
+ // did we already have a site set?
+ let alreadyHadSite = selectedSite != null;
+ pickingSiteForCategory = false;
+ selectedSite = e.features[0].properties.icao;
+ console.log(e.features);
+ stationLat = e.features[0].geometry.coordinates[1];
+ stationLong = e.features[0].geometry.coordinates[0];
+ if (!alreadyHadSite) {
+ selectedPrimaryLayer = null;
+ open = true;
+ } else {
+ if (map.getLayer("data")) {
+ map.removeLayer('data');
+ }
+ plotRadial(map, selectedSite, "REF", 1, stationLat, stationLong);
+ } // preserve the layer the user had if they click while already selected.
+ }
+ });
+ map.on('mouseenter', `select-${item.id}`, () => {
+ map.getCanvas().style.cursor = 'pointer';
+ });
+ map.on('mouseleave', `select-${item.id}`, () => {
+ map.getCanvas().style.cursor = '';
+ });
+ }
+
+ function selectStandard(layer: PrimaryLayer) {
+ selectedPrimaryLayer = layer.id;
+ idToNameCache[layer.id] = layer.layer;
+ pickingSiteForCategory = false;
+ currentlySelectingCategory = null;
+ selectedSite = null;
+ if (currentlySelectingFor) {
+ if (map.getLayer(`select-${currentlySelectingFor}`)) {
+ map.removeLayer(`select-${currentlySelectingFor}`);
+ }
+ }
+ if (map.getLayer("data")) {
+ map.removeLayer('data');
+ }
+ if (layer.type === 'raster') {
+
+ if (!map.getSource(layer.id)) {
+ map.addSource(layer.id, {
+ type: 'raster',
+ tiles: [
+ layer.tileUrl
+ ]
+ });
+ }
+ map.addLayer({
+ id: 'data',
+ type: 'raster',
+ source: `${layer.id}`
+ }, 'alerts');
+ }
+ }
@@ -62,30 +175,7 @@
Data available at {selectedSite}
{#each currentlySelectingCategory as data (data.id)}
- {
- selectedPrimaryLayer = data.id;
- idToNameCache[data.id] = data.layer;
-
- if (!map.getSource(`${selectedSite}-${data.id}`)) {
- idToTileUrlCache[data.id] = data.tileUrl;
- map.addSource(`${selectedSite}-${data.id}`, {
- type: 'raster',
- tiles: [
- data.tileUrl.replace('{site}', selectedSite)
- ]
- });
- }
- if (map.getLayer("data")) {
- map.removeLayer('data');
- }
- map.addLayer({
- id: 'data',
- type: 'raster',
- source: `${selectedSite}-${data.id}`
- }, 'alerts');
- }}>{data.layer}
+ {showSinglesite(data);}}>{data.layer}
{/each}
@@ -103,35 +193,7 @@
{#each item.children as layer (layer.id)}
{
- selectedPrimaryLayer = layer.id;
- idToNameCache[layer.id] = layer.layer;
- pickingSiteForCategory = false;
- currentlySelectingCategory = null;
- selectedSite = null;
- if (currentlySelectingFor) {
- if (map.getLayer(`select-${currentlySelectingFor}`)) {
- map.removeLayer(`select-${currentlySelectingFor}`);
- }
- }
- if (map.getLayer("data")) {
- map.removeLayer('data');
- }
- if (layer.type === 'raster') {
- if (!map.getSource(layer.id)) {
- map.addSource(layer.id, {
- type: 'raster',
- tiles: [
- layer.tileUrl
- ]
- });
- }
- map.addLayer({
- id: 'data',
- type: 'raster',
- source: `${layer.id}`
- }, 'alerts');
- }
-
+ selectStandard(layer)
}}
>
{layer.layer}
@@ -141,80 +203,12 @@
{:else}
{
- if (currentlySelectingFor) {
- if (map.getLayer(`select-${currentlySelectingFor}`)) {
- map.removeLayer(`select-${currentlySelectingFor}`);
- }
- }
- selectedPrimaryLayer = item.id;
- idToNameCache[item.id] = item.category;
- pickingSiteForCategory = true;
- currentlySelectingCategory = item.children;
- selectedSite = null;
- if (!map.getSource(item.id)) {
- map.addSource(item.id, {
- type: 'geojson',
- data: stationGeojson(stations)
- });
- }
- map.addLayer({
- id: `select-${item.id}`,
- type: 'symbol',
- source: item.id,
- layout: {
- 'icon-image': ['get', 'icon'],
- 'text-field': ['get', 'icao'],
- 'text-size': 13,
- 'icon-size': 0.23
- },
- paint: {
- 'text-color': '#000000'
- }
- });
- currentlySelectingFor = item.id;
- map.on('click', `select-${item.id}`, (e) => {
- if (e && e.features) {
- // did we already have a site set?
- let alreadyHadSite = selectedSite != null;
- pickingSiteForCategory = false;
- selectedSite = e.features[0].properties.icao;
- if (!alreadyHadSite) {
- selectedPrimaryLayer = null;
- open = true;
- } else {
- if (!map.getSource(`${selectedSite}-${selectedPrimaryLayer}`)) {
-
- map.addSource(`${selectedSite}-${selectedPrimaryLayer}`, {
- type: 'raster',
- tiles: [
- idToTileUrlCache[selectedPrimaryLayer].replace('{site}', selectedSite)
- ],
- });
- }
- if (map.getLayer("data")) {
- map.removeLayer('data');
- }
- map.addLayer({
- id: 'data',
- type: 'raster',
- source: `${selectedSite}-${selectedPrimaryLayer}`
- }, 'alerts');
- } // preserve the layer the user had if they click while already selected.
- }
- });
- map.on('mouseenter', `select-${item.id}`, () => {
- map.getCanvas().style.cursor = 'pointer';
- });
- map.on('mouseleave', `select-${item.id}`, () => {
- map.getCanvas().style.cursor = '';
- });
- }}>{item.category}... {selectSiteForSinglesite(item)}}>{item.category}...
{/if}
{/if}
{/each}
- {selectedPrimaryLayer = null; if (map.getLayer('data')) {map.removeLayer('data')}}}>
+
None
diff --git a/client/src/lib/map/map.ts b/client/src/lib/map/map.ts
new file mode 100644
index 0000000..f9c8d54
--- /dev/null
+++ b/client/src/lib/map/map.ts
@@ -0,0 +1,223 @@
+
+import maplibregl from "maplibre-gl";
+import { CifContainer } from '$lib/generated_interop/cifContainer';
+import fragmentSource from '$lib/map/fragment.glsl?raw';
+import vertexSource from '$lib/map/vertex.glsl?raw';
+import { degreesToRadians } from '@turf/turf';
+import type { DigitalRadarData, Radial } from '../generated_interop/digitalRadarData';
+import {degToRad} from "$lib/vincenty.js";
+import init_palette_parser_wasm, {parse_palette} from "$lib/map/pal/wxbox_pal";
+import {WXBOX_STANDARD_REF} from "$lib/map/default_palettes";
+import type { Map } from "maplibre-gl";
+
+const BELOW_THRESHOLD = -9999.0;
+const RANGE_FOLDED = -9998.0;
+
+export async function plotRadial(map: Map, site: string, product: string, elevation: number, lat: number, long: number) {
+ const r = await fetch(`http://localhost:3000/v2/nexrad/l2/${site}/${elevation}/${product}`);
+ const buf = await r.arrayBuffer();
+
+ const container = CifContainer.fromBinary(new Uint8Array(buf));
+
+ if (container.messageType.oneofKind == 'digitalRadarData') {
+ const drd: DigitalRadarData = container.messageType.digitalRadarData;
+
+ await init_palette_parser_wasm();
+
+ if (map.getLayer("data")) {
+ map.removeLayer('data');
+ }
+ const dataLayer = {
+ id: 'data',
+ type: 'custom',
+ maxZoom: 13,
+ onAdd(map, gl) {
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
+ gl.shaderSource(vertexShader, vertexSource);
+ gl.compileShader(vertexShader);
+
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+ gl.shaderSource(fragmentShader, fragmentSource);
+ gl.compileShader(fragmentShader);
+
+ this.program = gl.createProgram();
+ gl.attachShader(this.program, vertexShader);
+ gl.attachShader(this.program, fragmentShader);
+ gl.linkProgram(this.program);
+ gl.useProgram(this.program);
+
+ this.aPos = gl.getAttribLocation(this.program, 'a_pos');
+
+ gl.uniform1f(gl.getUniformLocation(this.program, 'radarLat'), lat);
+ gl.uniform1f(gl.getUniformLocation(this.program, 'radarLng'), long);
+
+ const vertexData: number[] = [];
+ const triangleAzimuthLookup: number[] = [];
+
+ const radarCoordinate = maplibregl.MercatorCoordinate.fromLngLat({
+ lng: long,
+ lat: lat
+ });
+
+ for (let i =0; i < drd.radials.length; i++) {
+ const radial = drd.radials[i];
+
+ if (radial.product && radial.product.data && radial.product.data.data) {
+ const angle_a = radial.azimuthAngleDegrees;
+ const angle_b = (i == drd.radials.length - 1) ? drd.radials[0].azimuthAngleDegrees : drd.radials[i+1].azimuthAngleDegrees;
+
+ const start_range = radial.product.data.startRange; // m
+ const sample_interval = radial.product.data.sampleInterval; // m
+ const number_of_samples = radial.product.data.data.length;
+ const range = sample_interval * number_of_samples + start_range; // m
+ // add an extra sample for good measure
+ const padded_range = range + sample_interval; // m
+
+ const in_weird_mercator_units = padded_range / radarCoordinate.meterInMercatorCoordinateUnits();
+ const pointAX = radarCoordinate.x + in_weird_mercator_units * Math.sin(degToRad(-angle_a + 180));
+ const pointAY = radarCoordinate.y + in_weird_mercator_units * Math.cos(degToRad(-angle_a + 180));
+ const pointA = new maplibregl.MercatorCoordinate(pointAX, pointAY, 0);
+
+ const pointBX = radarCoordinate.x + in_weird_mercator_units * Math.sin(degToRad(-angle_b + 180));
+ const pointBY = radarCoordinate.y + in_weird_mercator_units * Math.cos(degToRad(-angle_b + 180));
+ const pointB = new maplibregl.MercatorCoordinate(pointBX, pointBY, 0);
+
+ vertexData.push(radarCoordinate.x);
+ vertexData.push(radarCoordinate.y);
+ vertexData.push(pointA.x);
+ vertexData.push(pointA.y);
+ vertexData.push(pointB.x);
+ vertexData.push(pointB.y);
+ triangleAzimuthLookup.push(angle_a);
+ }
+ }
+
+ const radar_range_maximum = 560; // ish km
+ const degrees_of_lat = radar_range_maximum / 110.574;
+ const degrees_of_long =
+ radar_range_maximum / (111.32 * Math.cos(degreesToRadians(lat)));
+
+ this.buffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
+ gl.bufferData(
+ gl.ARRAY_BUFFER,
+ new Float32Array(vertexData),
+ gl.STATIC_DRAW
+ );
+ this.vertexCount = vertexData.length/2;
+ gl.uniform1fv(gl.getUniformLocation(this.program, 'triangleAzimuthLookup'), new Float32Array(triangleAzimuthLookup));
+
+ function scaleMomentData(radial: Radial, u: number): number {
+ if (!radial.product || !radial.product.data) return BELOW_THRESHOLD;
+ if (radial.product?.data?.scale == 0) {
+ return u;
+ } else {
+ if (u == 0) {
+ return BELOW_THRESHOLD;
+ } else if (u == 1) {
+ return RANGE_FOLDED;
+ } else {
+ return (u - radial.product.data?.offset) / radial.product?.data?.scale;
+ }
+ }
+ }
+ const raw_data = [];
+ let validRadials = 0;
+
+ for (const radial of drd.radials) {
+ if (radial.product && radial.product.data && radial.product.data.data) {
+ for (const datapoint of radial.product.data.data) {
+ raw_data.push(scaleMomentData(radial, datapoint));
+ }
+ validRadials++;
+ }
+ }
+
+ const data = new Float32Array(raw_data);
+ gl.activeTexture(gl.TEXTURE0 + 10);
+ this.texture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.R32F, 1832, validRadials, 0, gl.RED, gl.FLOAT, data);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.uniform1i(gl.getUniformLocation(this.program, 'data'), 10);
+
+ // parse, transform, and send over the palette
+ const palette = parse_palette(WXBOX_STANDARD_REF);
+ gl.uniform1i(gl.getUniformLocation(this.program, 'paletteLength'), palette.length);
+ const rboundsData: number[] = [];
+ const rcolorData: number[] = [];
+ const rcolor2Data: number[] = [];
+ for (const paletteEntry of palette) {
+ const [bounds, color] = paletteEntry;
+ rboundsData.push(...bounds);
+ rcolorData.push(...[color[0].red, color[0].green, color[0].blue, color[0].alpha]);
+ rcolor2Data.push(...[color[1].red, color[1].green, color[1].blue, color[1].alpha]);
+ }
+ // create the bounds texture first
+ const boundsData = new Float32Array(rboundsData);
+ gl.activeTexture(gl.TEXTURE0 + 11);
+ this.boundsTexture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, this.boundsTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, 1, palette.length, 0, gl.RG, gl.FLOAT, boundsData);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.uniform1i(gl.getUniformLocation(this.program, 'bounds'), 11);
+ // then the colorData texture
+ const colorData = new Uint8Array(rcolorData);
+ gl.activeTexture(gl.TEXTURE0 + 12);
+ this.colorDataTexture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, this.colorDataTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, palette.length, 0, gl.RGBA, gl.UNSIGNED_BYTE, colorData);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.uniform1i(gl.getUniformLocation(this.program, 'colorData'), 12);
+ // then the colorData2 texture
+ const color2Data = new Uint8Array(rcolor2Data);
+ gl.activeTexture(gl.TEXTURE0 + 13);
+ this.color2DataTexture = gl.createTexture();
+ gl.bindTexture(gl.TEXTURE_2D, this.color2DataTexture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 1, palette.length, 0, gl.RGBA, gl.UNSIGNED_BYTE, color2Data);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+ gl.uniform1i(gl.getUniformLocation(this.program, 'color2Data'), 13);
+ },
+ render(gl, args) {
+ gl.enable(gl.SAMPLE_COVERAGE);
+ gl.sampleCoverage(1.0, false);
+
+ gl.activeTexture(gl.TEXTURE0 + 10);
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+ gl.activeTexture(gl.TEXTURE0 + 11);
+ gl.bindTexture(gl.TEXTURE_2D, this.boundsTexture);
+ gl.activeTexture(gl.TEXTURE0 + 12);
+ gl.bindTexture(gl.TEXTURE_2D, this.colorDataTexture);
+ gl.activeTexture(gl.TEXTURE0 + 13);
+ gl.bindTexture(gl.TEXTURE_2D, this.color2DataTexture);
+ gl.useProgram(this.program);
+
+ gl.uniformMatrix4fv(
+ gl.getUniformLocation(this.program, 'u_matrix'),
+ false,
+ args.defaultProjectionData.mainMatrix
+ );
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
+ gl.enableVertexAttribArray(this.aPos);
+ gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
+ gl.enable(gl.BLEND);
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
+ }
+ };
+ map.addLayer(dataLayer, 'alerts');
+ }
+}
\ No newline at end of file