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 @@ - + + - - @@ -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