/**
* SHARPpy-style text parser (also CM1 input_sounding and Chaser Archive JSON).
* Produces the SharpJS sounding object used everywhere else:
*
* { Title, STitle, Location, Sounding: { P, H, T, D, W, TW, DW, Slice, OMEGA, SRW }, DWL, TWL, WWL, Len }
*
* Each Slice row is [pres mb, hght m, temp C, dwpt C, wdir deg, wspd kt, wetbulb C, omega].
* Pure functions: no DOM, no network.
*/
import { Lerp, clamp, VirtTemp, wetbulb } from '../calc/skewtCalcs.js';
import { lerp, KTStoMPS } from './general.js';
/**
* Calculate the fractional position of V between range endpoints B and T.
* @param {number} V Value to locate
* @param {number} B First endpoint
* @param {number} T Second endpoint
* @returns {number} Relative position within the range
*/
function GetNumFromRange(V, B, T) {
return (B - V) / (B - T);
}
/**
* Convert wind speed from meters per second to knots.
* @param {number} n Wind speed (in m/s).
* @returns {number} Wind speed (in kt).
*/
function MPStoKTS(n){
return n * 1.94384;
}
/**
* Estimate pressure one height increment above a level using the hypsometric equation.
* @param {number} pLower Pressure at the lower level (in hPa).
* @param {number} deltaZ Height increment (in m).
* @param {number} theta Potential temperature (in K).
* @param {number} qv_gkg Water-vapor mixing ratio (in g/kg).
* @returns {number} Pressure at the upper level (in hPa).
* @see https://glossary.ametsoc.org/wiki/hypsometric-equation/
*/
function calcNxtPrs(pLower, deltaZ, theta, qv_gkg) {
const Rd = 287.04;
const g = 9.80665;
const qv_kg = qv_gkg / 1000;
const tempK = theta * Math.pow(pLower / 1000, 287.04 / 1005.7);
const vTemp = tempK * (1 + qv_kg / 0.622) / (1 + qv_kg);
return pLower / Math.exp(deltaZ / (vTemp * (Rd / g)));
}
/**
* Convert potential temperature to air temperature at a pressure level.
* @param {number} theta Potential temperature (in K).
* @param {number} p Pressure (in hPa).
* @returns {number} Air temperature (in °C).
* @see https://glossary.ametsoc.org/wiki/potential-temperature/
*/
function tempCFromTheta(theta, p) {
return theta * Math.pow(p / 1000.0, 0.2854) - 273.15;
}
/**
* Derive dewpoint from water-vapor mixing ratio and pressure.
* @param {number} qv Water-vapor mixing ratio (in g/kg).
* @param {number} p Pressure (in hPa).
* @returns {number} Dewpoint temperature (in °C).
*/
function dewpointCFromQv(qv, p) {
const eps = 0.62195691;
const w = qv / 1000.0;
const e = (w * p) / (eps + w);
const ln = Math.log(Math.max(e, 1e-12) / 6.112);
return (243.5 * ln) / (17.67 - ln);
}
/**
* Convert CM1 surface metadata and profile rows into pressure-level records.
* @param {number[]} header Surface pressure (hPa), potential temperature (K), and mixing ratio (g/kg).
* @param {string[]} lines CM1 sounding lines; profile height is in m, theta in K, mixing ratio in g/kg, and wind components in m/s.
* @returns {object[]} Profile records with pressure (in hPa), height (in m), temperature/dewpoint (in °C), direction (in degrees), and speed (in m/s).
*/
function parseCM1Sounding(header, lines) {
const [sfcP, sfcTheta, sfcQV] = header;
let currentP = sfcP;
let currentZ = 0;
return lines.slice(1).map(line => {
const [hght, tta, qv, u, v] = line.trim().split(/\s+/).map(Number);
const dz = hght - currentZ;
const p = calcNxtPrs(currentP, dz, tta, qv);
const tmpC = tempCFromTheta(tta, p);
const dewC = dewpointCFromQv(qv, p);
const speed = Math.hypot(u, v);
const dir = (270 - Math.atan2(v, u) * 180 / Math.PI) % 360;
currentP = p;
currentZ = hght;
return {
p: p,
hght: hght,
tmpC: tmpC,
dewC: dewC,
dir: dir,
speed: speed,
};
});
}
/**
* Convert a CM1 input_sounding text file into SHARPpy raw sounding text.
* @param {string} Text CM1 sounding text.
* @returns {string} SHARPpy-format sounding text.
*/
function breakdownCM1(Text){
const lines = Text.trim().split('\n');
const header = lines[0].trim().split(/\s+/).map(Number);
const TrueProfiles = parseCM1Sounding(header, lines);
let InfoString = "";
for (let i = 0; i < TrueProfiles.length; i++) {
let PC = TrueProfiles[i].p;
let HC = TrueProfiles[i].hght;
let TC = TrueProfiles[i].tmpC;
let DC = TrueProfiles[i].dewC;
let WDC = TrueProfiles[i].dir;
let WSC = MPStoKTS(TrueProfiles[i].speed);
InfoString += ` ${PC},\t${HC},\t${TC},\t${DC},\t${WDC},\t${WSC},\t${0}\n`;
}
let Title = "No Title Provided";
let txt = `%TITLE%
${Title}
LEVEL HGHT TEMP DWPT WDIR WSPD OMEGA
--------------------------------------------------------------------------------
%RAW%
${InfoString}%END%`;
return txt;
}
/**
* Parse supported sounding text and construct the normalized SharpJS sounding object.
* @param {string} text SHARPpy, Chaser Archive JSON, or CM1 sounding text.
* @returns {object|false} Parsed sounding with level arrays and metadata (pressure in hPa, height in m, temperature/dewpoint in °C, direction in degrees, wind speed in kt), or false when text is null.
*/
export function Process(text) {
let Json = {
Sounding: {
T: [],
D: [],
W: [],
H: [],
P: [],
TW: [],
SRW: [],
DW: [],
Slice: [],
OMEGA: [],
},
Len: 0,
Location: false,
};
let Info = text;
if (Info == null) {
console.error("Error reading the file");
return false;
}
if (Info.includes('"level"')){
//ChaserArchive
let True_Info = JSON.parse(text);
let InfoString = "";
for (let i = 0; i < True_Info["hght"].length; i++) {
let PC = True_Info["level"][i];
let HC = True_Info["hght"][i];
let TC = True_Info["temp"][i];
let DC = True_Info["dwpt"][i];
let WDC = True_Info['wdir'][i];
let WSC = True_Info['wspd'][i];
InfoString += ` ${PC},\t${HC},\t${TC},\t${DC},\t${WDC},\t${WSC},\t${0}\n`;
}
let Title = "No Title Provided";
let txt = `%TITLE%
${Title}
LEVEL HGHT TEMP DWPT WDIR WSPD OMEGA
--------------------------------------------------------------------------------
%RAW%
${InfoString}%END%`;
Info = txt;
} else if(Info.includes("%START%")){
//SHARPPY
} else if (!/\p{L}/u.test(Info)) {
//CM1
Info = breakdownCM1(Info);
} else if (Info.includes("#")){
//ICGB: not supported yet
}
//Build the Return JSON Object
let Lines = Info.split("\n");
let Split = Info.split("%RAW%");
Json.Title = Lines[1];
Json.STitle = Lines[1];
if (Lines[1].indexOf("WRF") > -1 && Lines[1].indexOf("AT: ") > -1){
let SplitFinder = Lines[1].indexOf("N, ") > -1 ? Lines[1].indexOf("N, ") : Lines[1].indexOf("S, ")
let Lat = Lines[1].substring(Lines[1].indexOf("AT: "), SplitFinder)
let Lon = Lines[1].substring(SplitFinder, Lines[1].length)
if (Lat.indexOf("S")){
Lat = parseFloat(Lat.substring(4, Lat.length-2));
} else {
Lat = -parseFloat(Lat.substring(4, Lat.length-2));
}
if (Lon.indexOf("E")){
Lon = -(360-parseFloat(Lon.substring(2, Lon.length-2)))%360;
} else {
Lon = -parseFloat(Lon.substring(2, Lon.length-2))%360;
}
Json.Location = [Lat, Lon];
Json.Location2 = [Lat, -Lon];
}
Split = Split[1].split("%END%");
let PL = Split[0].split("\n");
// let PL = Pr[1].split("%END%");
Json.Len = PL.length;
let DWL = 0;
let TWL = 0;
let WWL = 0;
let SFCLayer = 9999;
let sfc = PL[1].split(",");
let GoodData = [];
for (let x = 0; x < sfc.length; x++) {
sfc[x] = parseFloat(sfc[x]);
}
for (let i = 0; i < PL.length; i++) {
let Ln = PL[i].split(",");
if (Ln.length == 1) {
continue;
}
for (let x = 0; x < Ln.length; x++) {
Ln[x] = parseFloat(Ln[x]);
}
if (Ln.length == 6){
Ln[Ln.length] = wetbulb(parseFloat(Ln[0]), parseFloat(Ln[2]), parseFloat(Ln[3]))
Ln[Ln.length] = 0
} else {
Ln.splice(Ln.length-1, 0, wetbulb(parseFloat(Ln[0]), parseFloat(Ln[2]), parseFloat(Ln[3])))
}
if (Ln[2] == -9999 && Ln[3] == -9999 && Ln[4] == -9999 && Ln[5] == -9999) {
continue;
}
if (Ln[2] != -9999 && Ln[3] != -9999 && Ln[4] != -9999 && Ln[5] != -9999) {
GoodData.push(Json.Sounding.Slice.length);
}
Json.Sounding.P.push(parseFloat(Ln[0]));
Json.Sounding.H.push(parseFloat(Ln[1]));
Json.Sounding.T.push(parseFloat(Ln[2]));
Json.Sounding.D.push(parseFloat(Ln[3]));
if (parseFloat(Ln[2]) != -9999 && !isNaN(parseFloat(Ln[2]))) {
Json.Sounding.TW.push(
{ T: parseFloat(Ln[2]), VT: VirtTemp(Ln, sfc[0], true, true), WB: Ln[6], P: parseFloat(Ln[0]), H: parseFloat(Ln[1]) }
);
TWL += 1;
SFCLayer = Math.min(SFCLayer, i);
};
if (parseFloat(Ln[3]) != -9999 && !isNaN(parseFloat(Ln[3]))) {
Json.Sounding.DW.push(
{ D: parseFloat(Ln[3]), P: parseFloat(Ln[0]), H: parseFloat(Ln[1]) }
);
DWL += 1;
SFCLayer = Math.min(SFCLayer, i);
};
Json.Sounding.Slice.push(Ln);
Json.Sounding.OMEGA.push(Ln[7]/10);
}
Json.DWL = DWL;
Json.TWL = TWL;
//Fix -9999 Missing data
// // console.log(Json.Sounding.Slice, GoodData);
let Iterator = 0;
let NewSlices = Json.Sounding.Slice;
if (GoodData.length < 1) {
} else {
let Bottom = Json.Sounding.Slice[GoodData[0]];
let Top = Json.Sounding.Slice[GoodData[1]];
for (let i = 0; i < Json.Sounding.Slice.length; i++) {
if (GoodData.indexOf(i) != -1) {
Bottom = Json.Sounding.Slice[i];
if ((GoodData.indexOf(i) + 1) >= GoodData.length) {
break;
}
Top = Json.Sounding.Slice[Math.min(Json.Sounding.Slice.length - 1, GoodData[GoodData.indexOf(i) + 1])];
} else {
let S = Json.Sounding.Slice[i];
let LerpFactor = clamp(GetNumFromRange(S[1], Bottom[1], Top[1]), 0, 1);
for (let a = 2; a < S.length; a++) {
if (S[a] == -9999) {
S[a] = Lerp(Bottom[a], Top[a], LerpFactor);
if (a == 3){
Json.Sounding.D[i] = Lerp(Bottom[a], Top[a], LerpFactor);
}
}
}
}
}
}
for (let i = 0; i < Json.Sounding.Slice.length; i++){
let P = Json.Sounding.Slice[i];
for (let x = 0; x < P.length; x++) {
let Drat = P[x];
if (Drat == -9999) {
if (i == Json.Sounding.Slice.length - 1) {
Drat = Json.Sounding.Slice[i - 1][x];
} else {
let Higher = i;
let Lower = i;
while (true) {
Lower -= 1;
if (Json.Sounding.Slice[Lower] == undefined) {
Lower += 1;
break;
}
if (Json.Sounding.Slice[Lower][x] != -9999 || Lower < 0) {
break;
}
}
while (true) {
Higher += 1;
if (Json.Sounding.Slice[Higher] == undefined) {
Higher -= 1;
break;
}
if (Json.Sounding.Slice[Higher][x] != -9999 || Higher > Json.Sounding.Slice.length) {
break;
}
}
let Lwr = Json.Sounding.Slice[Lower];
let Hgr = Json.Sounding.Slice[Higher];
if (Hgr[x] == -9999 && Json.Sounding.Slice.length-1 <= Higher && (x == 3 || Json.Sounding.Slice.length-1 == i)){
if (x == 3){
P[x] = Hgr[2] - Math.abs(Lwr[3] - Lwr[2]);
Json.Sounding.D[i] = clamp(P[x], -9999, Json.Sounding.T[i]);
Json.Sounding.Slice[i][x] = clamp(P[x], -9999, Json.Sounding.T[i]);
}
continue;
}
P[x] = lerp(
Hgr[x],
Lwr[x],
clamp(
(P[1] - Hgr[1]) / (Lwr[1] - Hgr[1]),
0,
1
)
)
}
}
}
}
for (let i = 0; i < Json.Sounding.Slice.length; i++) {
let Ln = Json.Sounding.Slice[i];
Json.Sounding.W.push(
{ WD: parseFloat(Ln[4]), WS: parseFloat(Ln[5]), H: parseFloat(Ln[1]) }
);
WWL += 1;
}
Json.WWL = WWL;
return Json;
}